├── mod_dev_settings ├── init.lua ├── mod.xml └── files │ └── magic_numbers.xml ├── screenshot.jpg ├── mod_ws_api ├── mod.xml ├── data │ └── ws │ │ ├── host.lua │ │ ├── pollws.lua │ │ ├── utils.lua │ │ ├── coroutines.lua │ │ ├── ws.lua │ │ └── json.lua └── init.lua ├── servers ├── .env.example ├── package.json ├── twitch_fragments │ ├── potion_material.lua │ ├── setup.lua │ └── outcomes.lua ├── www_console │ ├── css │ │ ├── themes │ │ │ ├── eclipse.css │ │ │ └── dracula.css │ │ └── codemirror-index.css │ ├── index.html │ ├── lib │ │ ├── xterm-addon-fit.js │ │ ├── modes │ │ │ └── lua │ │ │ │ ├── index.html │ │ │ │ └── lua.js │ │ ├── xterm.css │ │ └── codemirror.css │ └── js │ │ └── noitaconsole.js ├── main_console.js ├── main_twitch.js └── package-lock.json ├── LICENSE ├── .gitignore └── README.md /mod_dev_settings/init.lua: -------------------------------------------------------------------------------- 1 | ModMagicNumbersFileAdd( "mods/mod_dev_settings/files/magic_numbers.xml" ) -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probable-basilisk/noita-ws-api/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /mod_ws_api/mod.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /servers/.env.example: -------------------------------------------------------------------------------- 1 | # CHANNEL SETTINGS 2 | CHANNEL_NAME= 3 | 4 | # VOTE SETTINGS 5 | SECS_FOR_VOTE=90 6 | SECS_BETWEEN_VOTES=30 7 | -------------------------------------------------------------------------------- /mod_dev_settings/mod.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /mod_ws_api/data/ws/host.lua: -------------------------------------------------------------------------------- 1 | -- This file exists so that mods can conveniently override it to 2 | -- change the URL that ws-api attempts to connect to 3 | WS_HOST_URL = "ws://localhost:9090" 4 | 5 | function get_ws_host_url() 6 | return WS_HOST_URL 7 | end -------------------------------------------------------------------------------- /mod_dev_settings/files/magic_numbers.xml: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /servers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noita-ws-example-servers", 3 | "version": "0.0.1", 4 | "description": "Example Noita websocket API things", 5 | "main": "main_console.js", 6 | "author": "probable-basilisk", 7 | "license": "MIT", 8 | "dependencies": { 9 | "dotenv": "^5.0.1", 10 | "open": "^7.0.0", 11 | "polka": "^0.5.2", 12 | "serve-static": "^1.14.1", 13 | "tmi.js": "^1.4.2", 14 | "ws": "^5.1.0", 15 | "xterm": "^4.2.0-vscode1", 16 | "xterm-addon-fit": "^0.3.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /servers/twitch_fragments/potion_material.lua: -------------------------------------------------------------------------------- 1 | -- extracted from data/scripts/items/potion.lua 2 | potion_materials_standard = 3 | { 4 | "lava", 5 | "water", 6 | "blood", 7 | "alcohol", 8 | "oil", 9 | "slime", 10 | "acid", 11 | "radioactive_liquid", 12 | "gunpowder_unstable", 13 | "liquid_fire", 14 | "blood_cold" 15 | } 16 | 17 | potion_materials_magic = 18 | { 19 | "magic_liquid_teleportation", 20 | "magic_liquid_polymorph", 21 | "magic_liquid_random_polymorph", 22 | "magic_liquid_berserk", 23 | "magic_liquid_charm", 24 | "magic_liquid_invisibility" 25 | } 26 | -------------------------------------------------------------------------------- /mod_ws_api/init.lua: -------------------------------------------------------------------------------- 1 | function OnModPreInit() 2 | -- Nothing to do but this function has to exist 3 | end 4 | 5 | function OnModInit() 6 | -- Nothing to do but this function has to exist 7 | end 8 | 9 | function OnModPostInit() 10 | -- Nothing to do but this function has to exist 11 | end 12 | 13 | function OnWorldPreUpdate() 14 | -- Nothing to do but this function has to exist 15 | end 16 | 17 | function OnWorldPostUpdate() 18 | if _ws_main then _ws_main() end 19 | end 20 | 21 | function OnPlayerSpawned( player_entity ) 22 | dofile("data/ws/ws.lua") 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 probable-basilisk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /servers/www_console/css/themes/eclipse.css: -------------------------------------------------------------------------------- 1 | .cm-s-eclipse span.cm-meta {color: #FF1717;} 2 | .cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } 3 | .cm-s-eclipse span.cm-atom {color: #219;} 4 | .cm-s-eclipse span.cm-number {color: #164;} 5 | .cm-s-eclipse span.cm-def {color: #00f;} 6 | .cm-s-eclipse span.cm-variable {color: black;} 7 | .cm-s-eclipse span.cm-variable-2 {color: #0000C0;} 8 | .cm-s-eclipse span.cm-variable-3 {color: #0000C0;} 9 | .cm-s-eclipse span.cm-property {color: black;} 10 | .cm-s-eclipse span.cm-operator {color: black;} 11 | .cm-s-eclipse span.cm-comment {color: #3F7F5F;} 12 | .cm-s-eclipse span.cm-string {color: #2A00FF;} 13 | .cm-s-eclipse span.cm-string-2 {color: #f50;} 14 | .cm-s-eclipse span.cm-error {color: #f00;} 15 | .cm-s-eclipse span.cm-qualifier {color: #555;} 16 | .cm-s-eclipse span.cm-builtin {color: #30a;} 17 | .cm-s-eclipse span.cm-bracket {color: #cc7;} 18 | .cm-s-eclipse span.cm-tag {color: #170;} 19 | .cm-s-eclipse span.cm-attribute {color: #00c;} 20 | .cm-s-eclipse span.cm-link {color: #219;} 21 | 22 | .cm-s-eclipse .CodeMirror-matchingbracket { 23 | border:1px solid grey; 24 | color:black !important;; 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /servers/www_console/css/codemirror-index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | background-color: #000000; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | #content-area { 12 | overflow: hidden; 13 | position: relative; 14 | height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | width: 100%; 18 | will-change: overflow; 19 | } 20 | 21 | .console-column { 22 | width: 100%; 23 | border-right: 1px solid #888; 24 | overflow: auto; 25 | height: auto; 26 | } 27 | 28 | .log-column { 29 | background-color: #282a36; 30 | color: #aaa; 31 | border-left: 1px solid #888; 32 | overflow: auto; 33 | height: auto; 34 | flex: 1; 35 | padding-left: 10px; 36 | font-family: monospace; 37 | font-size: 8pt; 38 | } 39 | 40 | .log-item:nth-child(odd) { 41 | background-color: #1B1D29; 42 | } 43 | 44 | .message { 45 | font-weight: normal !important; 46 | } 47 | 48 | .error { 49 | color: #FF0000 !important; 50 | font-weight: bold; 51 | background-color: #000; 52 | } 53 | 54 | .row { 55 | flex: 1; 56 | margin: 10px; 57 | background-color: #111; 58 | } 59 | 60 | .linerow { 61 | flex: 0.05; 62 | margin: 10px; 63 | } -------------------------------------------------------------------------------- /servers/www_console/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | noitacon 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /servers/www_console/css/themes/dracula.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: dracula 4 | Author: Michael Kaminsky (http://github.com/mkaminsky11) 5 | 6 | Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme) 7 | 8 | */ 9 | 10 | 11 | .cm-s-dracula, .cm-s-dracula .CodeMirror-gutters { 12 | background-color: #282a36 !important; 13 | color: #f8f8f2 !important; 14 | border: none; 15 | } 16 | .cm-s-dracula .CodeMirror-gutters { color: #282a36; } 17 | .cm-s-dracula .CodeMirror-cursor { border-left: solid thin #f8f8f0; } 18 | .cm-s-dracula .CodeMirror-linenumber { color: #6D8A88; } 19 | .cm-s-dracula.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); } 20 | .cm-s-dracula .CodeMirror-line::selection, .cm-s-dracula .CodeMirror-line > span::selection, .cm-s-dracula .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); } 21 | .cm-s-dracula .CodeMirror-line::-moz-selection, .cm-s-dracula .CodeMirror-line > span::-moz-selection, .cm-s-dracula .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); } 22 | .cm-s-dracula span.cm-comment { color: #6272a4; } 23 | .cm-s-dracula span.cm-string, .cm-s-dracula span.cm-string-2 { color: #f1fa8c; } 24 | .cm-s-dracula span.cm-number { color: #bd93f9; } 25 | .cm-s-dracula span.cm-variable { color: white; } 26 | .cm-s-dracula span.cm-variable-2 { color: #50fa7b; } 27 | .cm-s-dracula span.cm-def { color: #ffb86c; } 28 | .cm-s-dracula span.cm-keyword { color: #ff79c6; } 29 | .cm-s-dracula span.cm-operator { color: #ff79c6; } 30 | .cm-s-dracula span.cm-keyword { color: #ff79c6; } 31 | .cm-s-dracula span.cm-atom { color: #bd93f9; } 32 | .cm-s-dracula span.cm-meta { color: #f8f8f2; } 33 | .cm-s-dracula span.cm-tag { color: #ff79c6; } 34 | .cm-s-dracula span.cm-attribute { color: #50fa7b; } 35 | .cm-s-dracula span.cm-qualifier { color: #50fa7b; } 36 | .cm-s-dracula span.cm-property { color: #66d9ef; } 37 | .cm-s-dracula span.cm-builtin { color: #50fa7b; } 38 | .cm-s-dracula span.cm-variable-3 { color: #50fa7b; } 39 | 40 | .cm-s-dracula .CodeMirror-activeline-background { background: rgba(255,255,255,0.1); } 41 | .cm-s-dracula .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } -------------------------------------------------------------------------------- /servers/www_console/lib/xterm-addon-fit.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(window,function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(){}return e.prototype.activate=function(e){this._terminal=e},e.prototype.dispose=function(){},e.prototype.fit=function(){var e=this.proposeDimensions();if(e&&this._terminal){var t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}},e.prototype.proposeDimensions=function(){if(this._terminal&&this._terminal.element&&this._terminal.element.parentElement){var e=this._terminal._core,t=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(t.getPropertyValue("height")),n=Math.max(0,parseInt(t.getPropertyValue("width"))),o=window.getComputedStyle(this._terminal.element),i=r-(parseInt(o.getPropertyValue("padding-top"))+parseInt(o.getPropertyValue("padding-bottom"))),a=n-(parseInt(o.getPropertyValue("padding-right"))+parseInt(o.getPropertyValue("padding-left")))-e.viewport.scrollBarWidth;return{cols:Math.max(2,Math.floor(a/e._renderService.dimensions.actualCellWidth)),rows:Math.max(1,Math.floor(i/e._renderService.dimensions.actualCellHeight))}}},e}();t.FitAddon=n}])}); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /servers/www_console/lib/modes/lua/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeMirror: Lua mode 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 | 27 |
28 |

Lua mode

29 |
70 | 76 | 77 |

Loosely based on Franciszek 78 | Wawrzak's CodeMirror 79 | 1 mode. One configuration parameter is 80 | supported, specials, to which you can provide an 81 | array of strings to have those identifiers highlighted with 82 | the lua-special style.

83 |

MIME types defined: text/x-lua.

84 | 85 |
86 | -------------------------------------------------------------------------------- /servers/twitch_fragments/setup.lua: -------------------------------------------------------------------------------- 1 | twitch_display_lines = {} 2 | gui = gui or GuiCreate() 3 | xpos = xpos or 80 4 | ypos = ypos or 30 5 | randomOnNoVotes = false 6 | math.randomseed(os.time()) 7 | 8 | function draw_twitch_display() 9 | GuiStartFrame( gui ) 10 | GuiLayoutBeginVertical( gui, xpos, ypos ) 11 | for idx, line in ipairs(twitch_display_lines) do 12 | GuiText(gui, 0, 0, line) 13 | end 14 | GuiLayoutEnd( gui ) 15 | end 16 | 17 | function set_countdown(timeleft) 18 | twitch_display_lines = {"Next vote: " .. timeleft .. "s"} 19 | end 20 | 21 | outcomes = {} 22 | function clear_outcomes() 23 | outcomes = {} 24 | end 25 | 26 | function clear_display() 27 | twitch_display_lines = {} 28 | end 29 | 30 | function add_outcome(outcome) 31 | table.insert(outcomes, outcome) 32 | end 33 | 34 | outcome_generators = {} 35 | 36 | function draw_outcomes(n) 37 | local candidates = {} 38 | for idx, generator in ipairs(outcome_generators) do 39 | candidates[idx] = {math.random(), generator} 40 | end 41 | table.sort(candidates, function(a, b) return a[1] < b[1] end) 42 | clear_outcomes() 43 | for idx = 1, n do 44 | add_outcome(candidates[idx][2]()) 45 | end 46 | end 47 | 48 | function update_outcome_display(vote_time_left) 49 | twitch_display_lines = {"Voting ends: " .. vote_time_left} 50 | for idx, outcome in ipairs(outcomes) do 51 | table.insert( 52 | twitch_display_lines, 53 | ("%d> %s (%d)"):format(idx, outcome.name, outcome.votes) 54 | ) 55 | end 56 | end 57 | 58 | function set_votes(outcome_votes) 59 | for idx, outcome in ipairs(outcomes) do 60 | outcome.votes = outcome_votes[idx] or 0 61 | end 62 | end 63 | 64 | function do_winner() 65 | local best_outcome = outcomes[1] 66 | for idx, outcome in ipairs(outcomes) do 67 | if outcome.votes > best_outcome.votes then 68 | best_outcome = outcome 69 | end 70 | end 71 | if best_outcome.votes > 0 then 72 | GamePrintImportant(best_outcome.name, best_outcome.desc or "you voted for this") 73 | best_outcome:func() 74 | elseif randomOnNoVotes then 75 | local random_outcome = outcomes[math.random(1, 4)] 76 | GamePrintImportant("Nobody voted!", "Random option choosen: " .. random_outcome.name) 77 | random_outcome:func() 78 | else 79 | GamePrintImportant("Nobody voted!", "Remember to vote!") 80 | end 81 | clear_outcomes() 82 | clear_display() 83 | end 84 | 85 | add_persistent_func("twitch_gui", draw_twitch_display) -------------------------------------------------------------------------------- /mod_ws_api/data/ws/pollws.lua: -------------------------------------------------------------------------------- 1 | if pollws then return end 2 | 3 | print("Attempting to link pollws.dll through FFI") 4 | local ffi = ffi or _G.ffi or require("ffi") 5 | ffi.cdef[[ 6 | struct pollsocket* pollws_open(const char* url); 7 | void pollws_close(struct pollsocket* ctx); 8 | int pollws_status(struct pollsocket* ctx); 9 | void pollws_send(struct pollsocket* ctx, const char* msg); 10 | int pollws_poll(struct pollsocket* ctx); 11 | unsigned int pollws_get(struct pollsocket* ctx, char* dest, unsigned int dest_size); 12 | unsigned int pollws_pop(struct pollsocket* ctx, char* dest, unsigned int dest_size); 13 | ]] 14 | print("CDEF was OK") 15 | pollws = ffi.load("pollws") 16 | print("FFI was OK") 17 | 18 | local POLLWS_STATUS_CODES = { 19 | [-1] = "invalid", 20 | [ 0] = "closed", 21 | [ 1] = "opening", 22 | [ 2] = "open", 23 | [ 3] = "error" 24 | } 25 | 26 | function open_socket(url, scratch_size) 27 | -- might as well have a comfortable megabyte of space 28 | if not scratch_size then scratch_size = 1000000 end 29 | local res = { 30 | _socket = pollws.pollws_open(url), 31 | _scratch = ffi.new("int8_t[?]", scratch_size), 32 | _scratch_size = scratch_size 33 | } 34 | function res:set_scratch_size(scratch_size) 35 | self._scratch = ffi.new("int8_t[?]", scratch_size) 36 | self._scratch_size = scratch_size 37 | end 38 | function res:poll() 39 | if not self._socket then return end 40 | local msg_size = pollws.pollws_pop(self._socket, self._scratch, self._scratch_size) 41 | if msg_size > 0 then 42 | local smsg = ffi.string(self._scratch, msg_size) 43 | return smsg 44 | else 45 | return nil 46 | end 47 | end 48 | function res:send(msg) 49 | if not self._socket then return end 50 | pollws.pollws_send(self._socket, msg) 51 | end 52 | function res:close() 53 | pollws.pollws_close(self._socket) 54 | self._socket = nil 55 | end 56 | function res:raw_status() 57 | if not self._socket then return -1 end 58 | return pollws.pollws_status(self._socket) 59 | end 60 | function res:status() 61 | return POLLWS_STATUS_CODES[self:raw_status()] or "unknown" 62 | end 63 | function res:run_async(on_message) 64 | self.running = true 65 | async(function() 66 | while self._socket and self.running do 67 | local msg = self:poll() 68 | if msg then on_message(self, msg) end 69 | wait(1) 70 | end 71 | end) 72 | end 73 | function res:stop() 74 | self.running = false 75 | end 76 | 77 | return res 78 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot of the example webconsole being used to print an "important" game message](/screenshot.jpg?raw=true) 2 | 3 | # noita-ws-api 4 | Websocket based API to Noita: allows Noita to connect through a websocket to a server, which can then execute 5 | arbitrary Lua within Noita's environment. 6 | 7 | ## Installation (mod / API host) 8 | 9 | The mod dynamically links the [pollws](https://github.com/probable-basilisk/pollws) library through 10 | luajit's FFI in order to act as a websocket client. 11 | 12 | * Grab the [pollws binaries](https://github.com/probable-basilisk/pollws/releases/download/0.1.0/pollws_0_1_0_windows.zip): copy 13 | the *32 bit* `pollws.dll` into your Noita install dir (so it should be in the same directory as `Noita.exe`). You don't need 14 | to care about the includes, bindings, or `.lib` files-- only the `.dll` is needed. 15 | 16 | * Copy the `mod_ws_api` directory into `{your_Noita_install_dir}/mods/`: you should end up with a file in 17 | `{your_Noita_install_dir}/mods/mod_ws_api/mod.xml`. 18 | 19 | * Enable the `mod_ws_api` mod in the Noita mods menu. It will ask for full permissions (in order to use the FFI); presumably 20 | you understand what that means and the risks involved. 21 | 22 | * (optional) Copy the `mod_dev_settings` directory into your mods dir. This mod disables Noita from pausing when 23 | it loses focus, which is very convenient when using the console. Enable this mod also in the Noita mods menu. 24 | 25 | ## Installation (example servers) 26 | 27 | The example servers run on nodejs, so get nodejs installed, and then 28 | ``` 29 | cd {the_example_servers_dir} 30 | npm install 31 | ``` 32 | 33 | ## Example: webconsole 34 | From the `servers` directory, run: 35 | ``` 36 | node main_console.js 37 | ``` 38 | 39 | A web browser window should automatically open to a Noita webconsole. Start up Noita (with `mod_ws` enabled), 40 | and then you can directly execute Lua commands from the webconsole. For example, try: 41 | 42 | ```Lua 43 | hello() 44 | GamePrintImportant("Big Message", "Sub Message") 45 | print_entity_info(get_player()) 46 | help("EntityGetTransform") 47 | ``` 48 | 49 | Note that there is rudimentary command completion with Tab, and that 50 | you can get help (if available) for Noita's API with `help("command")` or 51 | simply `command?`. 52 | 53 | ## Example: twitch interaction 54 | From the `servers` directory, run: 55 | ``` 56 | node main_twitch.js your_twitch_channel_name 57 | ``` 58 | 59 | Then start up Noita. You should see a voting thingy on the right side-- 60 | users in the specified twitch channel can type the appropriate numbers 61 | to vote on things to happen in your game. 62 | 63 | You can also create a .env file in your `servers`, to setup a default twitch account name to use, and the time for voting. See `servers/.env.example` for reference 64 | 65 | ## API Overview 66 | 67 | The `mod_ws` mod acts as a websocket client, and automatically tries to connect to `ws://localhost:9090`. 68 | 69 | * Websocket messages sent *TO* Noita are treated as raw Lua source and directly executed as Lua 70 | * Noita will reply with two kinds of messages: JSON payloads, and raw strings. Raw strings are 71 | prefixed with `>`. Approximately every second Noita will send a heartbeat message, in JSON, of the 72 | form `{"kind": "heartbeat", "source": "noita"}`. 73 | -------------------------------------------------------------------------------- /servers/main_console.js: -------------------------------------------------------------------------------- 1 | const ws = require('ws'); 2 | const crypto = require('crypto'); 3 | const open = require('open'); 4 | const WS_PORT = 9090; 5 | 6 | 7 | const wss = new ws.Server({ port: WS_PORT }); 8 | console.log("WS listening on " + WS_PORT); 9 | 10 | let noita = null; 11 | let con = null; 12 | 13 | function getConnectionName(ws) { 14 | return ws._socket.remoteAddress + ":" + ws._socket.remotePort; 15 | } 16 | 17 | function isConnectionLocalhost(ws) { 18 | const addr = ws._socket.remoteAddress 19 | return (addr == "::1") || (addr == "127.0.0.1") || (addr == "localhost") || (addr == "::ffff:127.0.0.1") 20 | } 21 | 22 | function randomToken() { 23 | return crypto.randomBytes(16).toString('hex'); 24 | } 25 | const TOKEN = randomToken(); 26 | 27 | wss.on('connection', function connection(ws) { 28 | const cname = getConnectionName(ws); 29 | console.log("New connection: " + cname); 30 | if (!isConnectionLocalhost(ws)) { 31 | console.log("Connection refused: not localhost!"); 32 | ws.terminate(); 33 | return 34 | } 35 | 36 | ws.on('message', function incoming(data, flags) { 37 | let jdata = null; 38 | // special case: if the string we get is prefixed with ">", 39 | // don't try to interpret as JSON, just treat it as a print 40 | if (data.slice(0, 1) == ">") { 41 | jdata = {kind: "print", text: data.slice(1)}; 42 | } else { 43 | try { 44 | jdata = JSON.parse(data); 45 | } catch (e) { 46 | console.log(data); 47 | console.error(e); 48 | return; 49 | } 50 | } 51 | 52 | if (jdata["kind"] === "heartbeat") { 53 | if (jdata["source"] === "noita") { 54 | if (noita != ws) { 55 | console.log("Registering noita!"); 56 | noita = ws; 57 | ws.send(`GamePrint('WS connected as ${cname}')`); 58 | ws.send("set_print_to_socket(true)"); 59 | ws.send("print('Noita connected')"); 60 | } 61 | } else if (jdata["source"] === "console") { 62 | if (con != ws) { 63 | if (jdata["token"] != TOKEN) { 64 | console.log("Invalid token! Got: " + jdata["token"]); 65 | ws.terminate(); 66 | return; 67 | } 68 | console.log("Registering console!"); 69 | con = ws; 70 | ws.send(JSON.stringify({kind: "print", text: `Connected as ${cname}`})); 71 | } 72 | } else { 73 | console.log("Unknown source: " + jdata["source"]); 74 | } 75 | } else if (ws === noita) { 76 | // send noita->con 77 | if (con != null) { 78 | con.send(JSON.stringify(jdata)); 79 | } 80 | } else if (ws === con) { 81 | // con -> noita 82 | if (noita != null) { 83 | // Don't want to involve JSON parser on Noita end, 84 | // so send just the raw command 85 | noita.send(jdata["command"]); 86 | } 87 | } 88 | }); 89 | ws.on('close', function closed(_ws, code, reason) { 90 | console.log("Connection closed: " + code + ", " + reason); 91 | if (ws === noita) { 92 | console.log("Noita closed"); 93 | noita = null; 94 | } 95 | if (ws === con) { 96 | console.log("Console webpage closed"); 97 | con = null; 98 | } 99 | }); 100 | }); 101 | 102 | const { join } = require('path'); 103 | const polka = require('polka'); 104 | 105 | const HTTP_PORT = 8080; 106 | const dir = join(__dirname, 'www_console'); 107 | const serve = require('serve-static')(dir); 108 | 109 | const secret_dir = "/" + TOKEN 110 | 111 | polka() 112 | .use(secret_dir, serve) 113 | .listen(HTTP_PORT, err => { 114 | if (err) throw err; 115 | console.log(`Console on localhost:${HTTP_PORT}${secret_dir}`); 116 | open(`http://localhost:${HTTP_PORT}${secret_dir}`); 117 | }); 118 | -------------------------------------------------------------------------------- /mod_ws_api/data/ws/utils.lua: -------------------------------------------------------------------------------- 1 | function hello() 2 | GamePrintImportant("Hello", "Hello") 3 | GamePrint("Hello") 4 | print("Hello") 5 | end 6 | 7 | function get_player() 8 | return EntityGetWithTag( "player_unit" )[1] 9 | end 10 | 11 | function get_player_pos() 12 | return EntityGetTransform(get_player()) 13 | end 14 | 15 | function get_closest_entity(px, py, tag) 16 | if not py then 17 | tag = px 18 | px, py = get_player_pos() 19 | end 20 | return EntityGetClosestWithTag( px, py, tag) 21 | end 22 | 23 | function get_entity_mouse(tag) 24 | local mx, my = DEBUG_GetMouseWorld() 25 | return get_closest_entity(mx, my, tag or "hittable") 26 | end 27 | 28 | function teleport(x, y) 29 | EntitySetTransform(get_player(), x, y) 30 | end 31 | 32 | function spawn_entity(ename, offset_x, offset_y) 33 | local x, y = get_player_pos() 34 | x = x + (offset_x or 0) 35 | y = y + (offset_y or 0) 36 | return EntityLoad(ename, x, y) 37 | end 38 | 39 | function print_component_info(c) 40 | local frags = {"<" .. ComponentGetTypeName(c) .. ">"} 41 | local members = ComponentGetMembers(c) 42 | if not members then return end 43 | for k, v in pairs(members) do 44 | table.insert(frags, k .. ': ' .. tostring(v)) 45 | end 46 | print(table.concat(frags, '\n')) 47 | end 48 | 49 | function get_vector_value(comp, member, kind) 50 | kind = kind or "float" 51 | local n = ComponentGetVectorSize( comp, member, kind ) 52 | if not n then return nil end 53 | local ret = {}; 54 | for i = 1, n do 55 | ret[i] = ComponentGetVectorValue(comp, member, kind, i-1) or "nil" 56 | end 57 | return ret 58 | end 59 | 60 | function print_vector_value(...) 61 | local v = get_vector_value(...) 62 | if not v then return nil end 63 | return "{" .. table.concat(v, ", ") .. "}" 64 | end 65 | 66 | function print_detailed_component_info(c) 67 | local members = ComponentGetMembers(c) 68 | if not members then return end 69 | local frags = {} 70 | for k, v in pairs(members) do 71 | if (not v) or #v == 0 then 72 | local mems = ComponentObjectGetMembers(c, k) 73 | if mems then 74 | table.insert(frags, k .. ">") 75 | for k2, v2 in pairs(mems) do 76 | table.insert(frags, " " .. k2 .. ": " .. tostring(v2)) 77 | end 78 | else 79 | v = print_vector_value(c, k) 80 | end 81 | end 82 | table.insert(frags, k .. ': ' .. tostring(v)) 83 | end 84 | print(table.concat(frags, '\n')) 85 | 86 | end 87 | 88 | function print_entity_info(e) 89 | local comps = EntityGetAllComponents(e) 90 | if not comps then 91 | print("Invalid entity?") 92 | return 93 | end 94 | for idx, comp in ipairs(comps) do 95 | print(comp, "-----------------") 96 | print_component_info(comp) 97 | end 98 | end 99 | 100 | function list_components(e) 101 | local comps = EntityGetAllComponents(e) 102 | if not comps then 103 | print("Invalid entity?") 104 | return 105 | end 106 | for idx, comp in ipairs(comps) do 107 | print(comp .. " : " .. ComponentGetTypeName(comp)) 108 | end 109 | end 110 | 111 | function list_funcs(filter) 112 | local ff = {} 113 | for k, v in pairs(getfenv()) do 114 | local first_letter = k:sub(1,1) 115 | if first_letter:upper() == first_letter then 116 | if (not filter) or k:lower():find(filter:lower()) then 117 | table.insert(ff, k) 118 | end 119 | end 120 | end 121 | table.sort(ff) 122 | print(table.concat(ff, "\n")) 123 | end 124 | 125 | function get_child_info(e) 126 | local children = EntityGetAllChildren(e) 127 | for _, child in ipairs(children) do 128 | print(child, EntityGetName(child) or "[no name]") 129 | end 130 | end 131 | 132 | function do_here(fn) 133 | local f = loadfile(fn) 134 | if type(f) ~= "function" then 135 | print("Loading error; check logger.txt for details.") 136 | end 137 | setfenv(f, getfenv()) 138 | f() 139 | end -------------------------------------------------------------------------------- /servers/www_console/lib/xterm.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * @license MIT 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | * Originally forked from (with the author's permission): 26 | * Fabrice Bellard's javascript vt100 for jslinux: 27 | * http://bellard.org/jslinux/ 28 | * Copyright (c) 2011 Fabrice Bellard 29 | * The original design remains. The terminal itself 30 | * has been extended to include xterm CSI codes, among 31 | * other features. 32 | */ 33 | 34 | /** 35 | * Default styles for xterm.js 36 | */ 37 | 38 | .xterm { 39 | font-feature-settings: "liga" 0; 40 | position: relative; 41 | user-select: none; 42 | -ms-user-select: none; 43 | -webkit-user-select: none; 44 | } 45 | 46 | .xterm.focus, 47 | .xterm:focus { 48 | outline: none; 49 | } 50 | 51 | .xterm .xterm-helpers { 52 | position: absolute; 53 | top: 0; 54 | /** 55 | * The z-index of the helpers must be higher than the canvases in order for 56 | * IMEs to appear on top. 57 | */ 58 | z-index: 5; 59 | } 60 | 61 | .xterm .xterm-helper-textarea { 62 | /* 63 | * HACK: to fix IE's blinking cursor 64 | * Move textarea out of the screen to the far left, so that the cursor is not visible. 65 | */ 66 | position: absolute; 67 | opacity: 0; 68 | left: -9999em; 69 | top: 0; 70 | width: 0; 71 | height: 0; 72 | z-index: -5; 73 | /** Prevent wrapping so the IME appears against the textarea at the correct position */ 74 | white-space: nowrap; 75 | overflow: hidden; 76 | resize: none; 77 | } 78 | 79 | .xterm .composition-view { 80 | /* TODO: Composition position got messed up somewhere */ 81 | background: #000; 82 | color: #FFF; 83 | display: none; 84 | position: absolute; 85 | white-space: nowrap; 86 | z-index: 1; 87 | } 88 | 89 | .xterm .composition-view.active { 90 | display: block; 91 | } 92 | 93 | .xterm .xterm-viewport { 94 | /* On OS X this is required in order for the scroll bar to appear fully opaque */ 95 | background-color: #000; 96 | overflow-y: scroll; 97 | cursor: default; 98 | position: absolute; 99 | right: 0; 100 | left: 0; 101 | top: 0; 102 | bottom: 0; 103 | } 104 | 105 | .xterm .xterm-screen { 106 | position: relative; 107 | } 108 | 109 | .xterm .xterm-screen canvas { 110 | position: absolute; 111 | left: 0; 112 | top: 0; 113 | } 114 | 115 | .xterm .xterm-scroll-area { 116 | visibility: hidden; 117 | } 118 | 119 | .xterm-char-measure-element { 120 | display: inline-block; 121 | visibility: hidden; 122 | position: absolute; 123 | top: 0; 124 | left: -9999em; 125 | line-height: normal; 126 | } 127 | 128 | .xterm { 129 | cursor: text; 130 | } 131 | 132 | .xterm.enable-mouse-events { 133 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ 134 | cursor: default; 135 | } 136 | 137 | .xterm.xterm-cursor-pointer { 138 | cursor: pointer; 139 | } 140 | 141 | .xterm.column-select.focus { 142 | /* Column selection mode */ 143 | cursor: crosshair; 144 | } 145 | 146 | .xterm .xterm-accessibility, 147 | .xterm .xterm-message { 148 | position: absolute; 149 | left: 0; 150 | top: 0; 151 | bottom: 0; 152 | right: 0; 153 | z-index: 10; 154 | color: transparent; 155 | } 156 | 157 | .xterm .live-region { 158 | position: absolute; 159 | left: -9999px; 160 | width: 1px; 161 | height: 1px; 162 | overflow: hidden; 163 | } 164 | 165 | .xterm-dim { 166 | opacity: 0.5; 167 | } 168 | 169 | .xterm-underline { 170 | text-decoration: underline; 171 | } 172 | -------------------------------------------------------------------------------- /servers/main_twitch.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const ws = require('ws'); 3 | const tmi = require('tmi.js'); 4 | const fs = require('fs'); 5 | 6 | // eh... 7 | const CHANNEL = process.argv[2] || process.env.CHANNEL_NAME; 8 | 9 | const WS_PORT = 9090; 10 | const SECS_BETWEEN_VOTES = process.env.SECS_BETWEEN_VOTES || 10; 11 | const SECS_FOR_VOTE = process.env.SECS_FOR_VOTE || 60; 12 | 13 | const wss = new ws.Server({ port: WS_PORT }); 14 | console.log("WS listening on " + WS_PORT); 15 | 16 | let noita = null; 17 | 18 | function getConnectionName(ws) { 19 | return ws._socket.remoteAddress + ":" + ws._socket.remotePort; 20 | } 21 | 22 | function isConnectionLocalhost(ws) { 23 | const addr = ws._socket.remoteAddress 24 | return (addr == "::1") || (addr == "127.0.0.1") || (addr == "localhost") || (addr == "::ffff:127.0.0.1") 25 | } 26 | 27 | function noitaDoFile(fn) { 28 | if (noita == null) { 29 | return; 30 | } 31 | const fdata = fs.readFileSync(fn); 32 | noita.send(fdata); 33 | } 34 | 35 | // TODO: refactor all this into a common file 36 | wss.on('connection', function connection(ws) { 37 | const cname = getConnectionName(ws); 38 | console.log("New connection: " + cname); 39 | if (!isConnectionLocalhost(ws)) { 40 | console.log("Connection refused: not localhost!"); 41 | ws.terminate(); 42 | return 43 | } 44 | 45 | ws.on('message', function incoming(data, flags) { 46 | let jdata = null; 47 | // special case: if the string we get is prefixed with ">", 48 | // don't try to interpret as JSON, just treat it as a print 49 | if (data.slice(0, 1) == ">") { 50 | console.log(data); 51 | return; 52 | } else { 53 | try { 54 | jdata = JSON.parse(data); 55 | } catch (e) { 56 | console.log(data); 57 | console.error(e); 58 | return; 59 | } 60 | } 61 | 62 | if (jdata["kind"] === "heartbeat") { 63 | if (noita != ws) { 64 | console.log("Registering noita!"); 65 | noita = ws; 66 | ws.send(`GamePrint('WS connected as ${cname}')`); 67 | ws.send("set_print_to_socket(true)"); 68 | noitaDoFile("twitch_fragments/setup.lua"); 69 | noitaDoFile("twitch_fragments/potion_material.lua"); 70 | noitaDoFile("twitch_fragments/outcomes.lua"); 71 | } 72 | } 73 | }); 74 | 75 | ws.on('close', function closed(_ws, code, reason) { 76 | console.log("Connection closed: " + code + ", " + reason); 77 | if (ws === noita) { 78 | console.log("Noita closed"); 79 | noita = null; 80 | } 81 | }); 82 | }); 83 | 84 | let timeleft = 0; 85 | let user_votes = {}; 86 | 87 | function getVotes() { 88 | let votes = [0, 0, 0, 0]; 89 | for(uname in user_votes) { 90 | let v = user_votes[uname]; 91 | if (v >= 1 && v <= 4) { 92 | votes[v-1] += 1; 93 | } 94 | } 95 | return votes; 96 | } 97 | 98 | let accepting_votes = false; 99 | function handleVote(username, v) { 100 | if(!accepting_votes) { 101 | return; 102 | } 103 | let iv = parseInt(v.slice(0,1)); 104 | if(isNaN(iv)) { 105 | return; 106 | } 107 | user_votes[username] = iv; 108 | } 109 | 110 | function sleep(nsecs) { 111 | return new Promise(resolve => setTimeout(resolve, nsecs * 1000)); 112 | } 113 | 114 | async function doQuestion() { 115 | // main timer 116 | timeleft = SECS_BETWEEN_VOTES; 117 | while(timeleft > 0) { 118 | if (noita == null) { 119 | return; 120 | } 121 | noita.send(`set_countdown(${timeleft})`); 122 | timeleft -= 1; 123 | await sleep(1); 124 | } 125 | if (noita == null) { 126 | return; 127 | } 128 | accepting_votes = true; 129 | user_votes = {}; 130 | noita.send("clear_display()\ndraw_outcomes(4)"); 131 | timeleft = SECS_FOR_VOTE; 132 | while(timeleft > 0) { 133 | if (noita == null) { 134 | return; 135 | } 136 | noita.send(`set_votes{${getVotes().join(",")}}`); 137 | noita.send(`update_outcome_display(${timeleft})`); 138 | timeleft -= 1; 139 | await sleep(1); 140 | } 141 | if (noita == null) { 142 | return; 143 | } 144 | noita.send("do_winner()"); 145 | } 146 | 147 | async function gameLoop() { 148 | while(true) { 149 | accepting_votes = false; 150 | while(noita == null) { 151 | console.log("Waiting for Noita..."); 152 | await sleep(5); 153 | } 154 | await doQuestion(); 155 | } 156 | } 157 | 158 | const options = { 159 | options: {debug: true}, 160 | connection: {reconnect: true} 161 | //identity: { username: process.env.TWITCH_USERNAME, password: process.env.TWITCH_OAUTH} 162 | }; 163 | 164 | const chatClient = new tmi.client(options); 165 | chatClient.connect().then((_client) => { 166 | chatClient.join(CHANNEL); 167 | 168 | chatClient.on("message", function (channel, userstate, message, self) { 169 | // console.log(message); 170 | // console.log(userstate); 171 | if (self) return; // Don't listen to my own messages.. 172 | if (userstate["message-type"] === "chat") { 173 | handleVote(userstate['user-id'], message); 174 | } 175 | }); 176 | }); 177 | 178 | gameLoop(); -------------------------------------------------------------------------------- /mod_ws_api/data/ws/coroutines.lua: -------------------------------------------------------------------------------- 1 | if async then return end -- guard against multiple "includes" 2 | 3 | -- coroutine runtime --------------------------------------------------------- 4 | -- FIXME: stolen from https://github.com/ecbambrick/UntitledGame/tree/master/src/lib 5 | 6 | 7 | -- This file implements wait, wait_signal, signal, and their supporting stuff. 8 | 9 | -- This table is indexed by coroutine and simply contains the time at which the coroutine 10 | -- should be woken up. 11 | local WAITING_ON_TIME = {} 12 | 13 | -- This table is indexed by signal and contains list of coroutines that are waiting 14 | -- on a given signal 15 | local WAITING_ON_SIGNAL = {} 16 | local WAITING_ON_SIGNAL_CALLBACK = {} 17 | 18 | -- Keep track of how long the game has been running. 19 | local CURRENT_TIME = 0 20 | 21 | function wait(frames) 22 | -- Grab a reference to the current running coroutine 23 | local co = coroutine.running() 24 | 25 | -- If co is nil, that means we're on the main process, which isn't a coroutine and can't yield 26 | assert(co ~= nil, "The main thread cannot wait!") 27 | 28 | -- Store the coroutine and its wakeup time in the WAITING_ON_TIME table 29 | local wakeupTime = CURRENT_TIME + frames 30 | WAITING_ON_TIME[co] = wakeupTime 31 | 32 | -- And suspend the process 33 | return coroutine.yield(co) 34 | end 35 | 36 | function wake_up_waiting_threads(frames_delta) 37 | -- this function should be called once per game logic update with the amount of time 38 | -- that has passed since it was last called 39 | CURRENT_TIME = CURRENT_TIME + frames_delta 40 | 41 | local threadsToWake = {} 42 | for co, wakeupTime in pairs(WAITING_ON_TIME) do 43 | if wakeupTime < CURRENT_TIME then 44 | table.insert(threadsToWake, co) 45 | end 46 | end 47 | 48 | for _, co in ipairs(threadsToWake) do 49 | WAITING_ON_TIME[co] = nil -- setting a field to nil removes it from the table 50 | local happy, err = coroutine.resume(co) 51 | if not happy then (cprint or print)("Coroutine error: " .. tostring(err)) end 52 | end 53 | end 54 | 55 | function wait_signal(signalName) 56 | -- Same check as in wait; the main thread cannot wait 57 | local co = coroutine.running() 58 | assert(co ~= nil, "The main thread cannot wait!") 59 | 60 | if WAITING_ON_SIGNAL[signalStr] == nil then 61 | -- If there wasn't already a list for this signal, start a new one. 62 | WAITING_ON_SIGNAL[signalName] = { co } 63 | else 64 | table.insert(WAITING_ON_SIGNAL[signalName], co) 65 | end 66 | 67 | return coroutine.yield() 68 | end 69 | 70 | function wait_signal_callback(signalName, callback) 71 | -- Same check as in wait; the main thread cannot wait 72 | local co = coroutine.running() 73 | assert(co ~= nil, "The main thread cannot wait!") 74 | 75 | if WAITING_ON_SIGNAL[signalStr] == nil then 76 | -- If there wasn't already a list for this signal, start a new one. 77 | WAITING_ON_SIGNAL[signalName] = { co } 78 | else 79 | table.insert(WAITING_ON_SIGNAL[signalName], co) 80 | end 81 | 82 | if WAITING_ON_SIGNAL_CALLBACK[signalStr] == nil then 83 | -- If there wasn't already a list for this signal, start a new one. 84 | WAITING_ON_SIGNAL_CALLBACK[signalName] = { callback } 85 | else 86 | table.insert(WAITING_ON_SIGNAL_CALLBACK[signalName], callback) 87 | end 88 | 89 | return coroutine.yield() 90 | end 91 | 92 | function signal(signalName) 93 | local threads = WAITING_ON_SIGNAL[signalName] 94 | if threads == nil then return end 95 | 96 | -- call the callback 97 | local funcs = WAITING_ON_SIGNAL_CALLBACK[signalName] 98 | if funcs ~= nil then 99 | WAITING_ON_SIGNAL_CALLBACK[signalName] = nil 100 | for _, func in ipairs(funcs) do 101 | func() 102 | end 103 | end 104 | 105 | -- resume the coroutine 106 | WAITING_ON_SIGNAL[signalName] = nil 107 | for _, co in ipairs(threads) do 108 | coroutine.resume(co) 109 | end 110 | end 111 | 112 | function signal_2params(signalName, a, b) 113 | local threads = WAITING_ON_SIGNAL[signalName] 114 | if threads == nil then return end 115 | 116 | -- call the callback 117 | local funcs = WAITING_ON_SIGNAL_CALLBACK[signalName] 118 | if funcs ~= nil then 119 | WAITING_ON_SIGNAL_CALLBACK[signalName] = nil 120 | for _, func in ipairs(funcs) do 121 | func() 122 | end 123 | end 124 | 125 | -- resume the coroutine 126 | WAITING_ON_SIGNAL[signalName] = nil 127 | for _, co in ipairs(threads) do 128 | coroutine.resume(co, a, b) 129 | end 130 | end 131 | 132 | 133 | function async(func) 134 | -- This function is just a quick wrapper to start a coroutine. 135 | local co = coroutine.create(func) 136 | return coroutine.resume(co) 137 | end 138 | 139 | function async_loop(func) 140 | -- This function is just a quick wrapper to start a coroutine. 141 | local func_loop = function() 142 | while true do 143 | func() 144 | end 145 | end 146 | 147 | local co = coroutine.create(func_loop) 148 | return coroutine.resume(co) 149 | end -------------------------------------------------------------------------------- /servers/www_console/js/noitaconsole.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | initCodeMirror(); 3 | initConnection(); 4 | }); 5 | 6 | var codeWindow = null; 7 | var connection = null; 8 | var connected = false; 9 | var repl = null; 10 | var lineWindow = null; 11 | var fit = null; 12 | 13 | var commandHistory = []; 14 | 15 | function ansiRGB(r, g, b) { 16 | return `\x1b[38;2;${r};${g};${b}m` 17 | } 18 | 19 | function ansiBgRGB(r, g, b) { 20 | return `\x1b[48;2;${r};${g};${b}m` 21 | } 22 | 23 | function ansiReset() { 24 | return '\x1b[0m' 25 | } 26 | 27 | 28 | function getToken() { 29 | var parts = document.URL.split("/"); 30 | if (parts[parts.length - 1].length == 32) { 31 | return parts[parts.length - 1] 32 | } else { 33 | return parts[parts.length - 2] 34 | } 35 | } 36 | 37 | function initConnection(url) { 38 | if(!url) { 39 | url = "ws://localhost:9090" 40 | } 41 | console.log("Connecting to url " + url); 42 | 43 | connection = new WebSocket(url); 44 | connection.addEventListener('open', function (event) { 45 | console.log('Connected: ' + url); 46 | connected = true; 47 | connection.send(JSON.stringify({kind: 'heartbeat', source: 'console', token: getToken()})); 48 | }); 49 | 50 | connection.addEventListener('message', function (event) { 51 | var jdata = JSON.parse(event.data); 52 | if (jdata["kind"] === "print") { 53 | replPrint(jdata["text"]); 54 | } else { 55 | console.log("Unknown message: " + event.data) 56 | } 57 | }); 58 | } 59 | 60 | function remoteEval(code) { 61 | if(code[0] === '!') { 62 | let parts = code.slice(1).split(" "); 63 | if(parts[0] === "connect") { 64 | initConnection(parts[1]); 65 | } else { 66 | replPrint("Unknown local command: " + parts[0]); 67 | } 68 | } else if(connected) { 69 | connection.send(JSON.stringify({kind: "command", command: code})); 70 | } 71 | } 72 | 73 | function longestSharedPrefix(a, b) { 74 | let nchars = Math.min(a.length, b.length); 75 | let prefixLength = 0; 76 | for(prefixLength = 0; prefixLength < nchars; ++prefixLength) { 77 | if(a[prefixLength] != b[prefixLength]) { 78 | break; 79 | } 80 | } 81 | return a.slice(0, prefixLength); 82 | } 83 | 84 | function longestSetPrefix(opts) { 85 | if(opts.length == 0) { 86 | return "" 87 | } 88 | let curPrefix = opts[0]; 89 | for(let idx = 1; idx < opts.length; ++idx) { 90 | curPrefix = longestSharedPrefix(curPrefix, opts[idx]); 91 | } 92 | return curPrefix; 93 | } 94 | 95 | function replPrint(message) { 96 | message = message.replace(/\n/g, "\r\n"); 97 | if(message.indexOf("ERR>") == 0) { 98 | message = ansiRGB(200, 0, 0) + message + ansiReset(); 99 | } else if(message.indexOf("RES>") == 0) { 100 | message = ansiRGB(100, 100, 255) + message + ansiReset(); 101 | } else if(message.indexOf("EVAL>") == 0) { 102 | message = ansiRGB(100, 255, 100) + message + ansiReset(); 103 | } else if(message.indexOf("COM>") == 0) { 104 | // see how many suggestions we got 105 | let parts = message.slice(4).split(" "); 106 | let prefix = parts[0]; 107 | let opts = parts[1].split(","); 108 | if(opts.length == 1 && opts[0] != "") { 109 | // fill in this completion 110 | lineWindow.setValue(prefix + opts[0]); 111 | lineWindow.setCursor(lineWindow.lineCount(), 0); 112 | } else if(opts.length > 1) { 113 | let completion = longestSetPrefix(opts); 114 | lineWindow.setValue(prefix + completion); 115 | lineWindow.setCursor(lineWindow.lineCount(), 0); 116 | } 117 | message = ansiRGB(150, 150, 150) + message + ansiReset(); 118 | } 119 | repl.writeln(message); 120 | } 121 | 122 | function initCodeMirror() { 123 | codeWindow = CodeMirror.fromTextArea(document.getElementById("code"), { 124 | value: "-- Put multiline Lua stuff here\n", 125 | mode: "lua", 126 | theme: "dracula", 127 | lineNumbers: true 128 | }); 129 | 130 | codeWindow.setOption("extraKeys", { 131 | "Shift-Enter": function(cm) { 132 | remoteEval(cm.getValue()); 133 | replPrint("EVAL> [buffer]"); 134 | } 135 | }); 136 | 137 | lineWindow = CodeMirror.fromTextArea(document.getElementById("replinput"), { 138 | value: "", 139 | mode: "lua", 140 | theme: "dracula" 141 | }); 142 | 143 | lineWindow.setOption("extraKeys", { 144 | "Enter": function(cm) { 145 | let val = cm.getValue(); 146 | if(val == "") { 147 | return; 148 | } 149 | if(val.slice(-1) == "?") { 150 | val = 'help("' + val.slice(0,-1) + '")' 151 | } 152 | remoteEval(val); 153 | if(commandHistory.indexOf(val) < 0) { 154 | commandHistory.push(val); 155 | } 156 | replPrint("EVAL> " + val); 157 | cm.setValue(""); 158 | }, 159 | "Tab": function(cm) { 160 | console.log("TAB"); 161 | const val = cm.getValue().trim(); 162 | if(val == "") { 163 | return; 164 | } 165 | // Note that [=[ some string ]=] is a special Lua 166 | // string literal that allows nesting of other string 167 | // literals, including the more typical [[ ]] pair 168 | remoteEval(`complete([=[${val}]=])`); 169 | }, 170 | "Up": function(cm) { 171 | let hpos = commandHistory.indexOf(cm.getValue()); 172 | if(hpos > 0) { 173 | cm.setValue(commandHistory[hpos-1]); 174 | } else if(hpos == 0) { 175 | // don't do anything 176 | } else { 177 | cm.setValue(commandHistory[commandHistory.length-1]); 178 | } 179 | }, 180 | "Down": function(cm) { 181 | let hpos = commandHistory.indexOf(cm.getValue()); 182 | if(hpos >= 0 && hpos < commandHistory.length - 1) { 183 | cm.setValue(commandHistory[hpos+1]); 184 | } 185 | } 186 | }); 187 | 188 | repl = new Terminal({ 189 | theme: { 190 | background: '#111' 191 | } 192 | }); 193 | fit = new FitAddon.FitAddon(); 194 | repl.loadAddon(fit); 195 | repl.open(document.getElementById("repl")); 196 | repl._initialized = true; 197 | 198 | fit.fit(); 199 | 200 | repl.writeln('Noita console'); 201 | repl.writeln('(Note that this panel is for output only)'); 202 | repl.writeln(''); 203 | } 204 | -------------------------------------------------------------------------------- /servers/www_console/lib/modes/lua/lua.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | // LUA mode. Ported to CodeMirror 2 from Franciszek Wawrzak's 5 | // CodeMirror 1 mode. 6 | // highlights keywords, strings, comments (no leveling supported! ("[==[")), tokens, basic indenting 7 | 8 | (function(mod) { 9 | if (typeof exports == "object" && typeof module == "object") // CommonJS 10 | mod(require("../../lib/codemirror")); 11 | else if (typeof define == "function" && define.amd) // AMD 12 | define(["../../lib/codemirror"], mod); 13 | else // Plain browser env 14 | mod(CodeMirror); 15 | })(function(CodeMirror) { 16 | "use strict"; 17 | 18 | CodeMirror.defineMode("lua", function(config, parserConfig) { 19 | var indentUnit = config.indentUnit; 20 | 21 | function prefixRE(words) { 22 | return new RegExp("^(?:" + words.join("|") + ")", "i"); 23 | } 24 | function wordRE(words) { 25 | return new RegExp("^(?:" + words.join("|") + ")$", "i"); 26 | } 27 | var specials = wordRE(parserConfig.specials || []); 28 | 29 | // long list of standard functions from lua manual 30 | var builtins = wordRE([ 31 | "_G","_VERSION","assert","collectgarbage","dofile","error","getfenv","getmetatable","ipairs","load", 32 | "loadfile","loadstring","module","next","pairs","pcall","print","rawequal","rawget","rawset","require", 33 | "select","setfenv","setmetatable","tonumber","tostring","type","unpack","xpcall", 34 | 35 | "coroutine.create","coroutine.resume","coroutine.running","coroutine.status","coroutine.wrap","coroutine.yield", 36 | 37 | "debug.debug","debug.getfenv","debug.gethook","debug.getinfo","debug.getlocal","debug.getmetatable", 38 | "debug.getregistry","debug.getupvalue","debug.setfenv","debug.sethook","debug.setlocal","debug.setmetatable", 39 | "debug.setupvalue","debug.traceback", 40 | 41 | "close","flush","lines","read","seek","setvbuf","write", 42 | 43 | "io.close","io.flush","io.input","io.lines","io.open","io.output","io.popen","io.read","io.stderr","io.stdin", 44 | "io.stdout","io.tmpfile","io.type","io.write", 45 | 46 | "math.abs","math.acos","math.asin","math.atan","math.atan2","math.ceil","math.cos","math.cosh","math.deg", 47 | "math.exp","math.floor","math.fmod","math.frexp","math.huge","math.ldexp","math.log","math.log10","math.max", 48 | "math.min","math.modf","math.pi","math.pow","math.rad","math.random","math.randomseed","math.sin","math.sinh", 49 | "math.sqrt","math.tan","math.tanh", 50 | 51 | "os.clock","os.date","os.difftime","os.execute","os.exit","os.getenv","os.remove","os.rename","os.setlocale", 52 | "os.time","os.tmpname", 53 | 54 | "package.cpath","package.loaded","package.loaders","package.loadlib","package.path","package.preload", 55 | "package.seeall", 56 | 57 | "string.byte","string.char","string.dump","string.find","string.format","string.gmatch","string.gsub", 58 | "string.len","string.lower","string.match","string.rep","string.reverse","string.sub","string.upper", 59 | 60 | "table.concat","table.insert","table.maxn","table.remove","table.sort" 61 | ]); 62 | var keywords = wordRE(["and","break","elseif","false","nil","not","or","return", 63 | "true","function", "end", "if", "then", "else", "do", 64 | "while", "repeat", "until", "for", "in", "local" ]); 65 | 66 | var indentTokens = wordRE(["function", "if","repeat","do", "\\(", "{"]); 67 | var dedentTokens = wordRE(["end", "until", "\\)", "}"]); 68 | var dedentPartial = prefixRE(["end", "until", "\\)", "}", "else", "elseif"]); 69 | 70 | function readBracket(stream) { 71 | var level = 0; 72 | while (stream.eat("=")) ++level; 73 | stream.eat("["); 74 | return level; 75 | } 76 | 77 | function normal(stream, state) { 78 | var ch = stream.next(); 79 | if (ch == "-" && stream.eat("-")) { 80 | if (stream.eat("[") && stream.eat("[")) 81 | return (state.cur = bracketed(readBracket(stream), "comment"))(stream, state); 82 | stream.skipToEnd(); 83 | return "comment"; 84 | } 85 | if (ch == "\"" || ch == "'") 86 | return (state.cur = string(ch))(stream, state); 87 | if (ch == "[" && /[\[=]/.test(stream.peek())) 88 | return (state.cur = bracketed(readBracket(stream), "string"))(stream, state); 89 | if (/\d/.test(ch)) { 90 | stream.eatWhile(/[\w.%]/); 91 | return "number"; 92 | } 93 | if (/[\w_]/.test(ch)) { 94 | stream.eatWhile(/[\w\\\-_.]/); 95 | return "variable"; 96 | } 97 | return null; 98 | } 99 | 100 | function bracketed(level, style) { 101 | return function(stream, state) { 102 | var curlev = null, ch; 103 | while ((ch = stream.next()) != null) { 104 | if (curlev == null) {if (ch == "]") curlev = 0;} 105 | else if (ch == "=") ++curlev; 106 | else if (ch == "]" && curlev == level) { state.cur = normal; break; } 107 | else curlev = null; 108 | } 109 | return style; 110 | }; 111 | } 112 | 113 | function string(quote) { 114 | return function(stream, state) { 115 | var escaped = false, ch; 116 | while ((ch = stream.next()) != null) { 117 | if (ch == quote && !escaped) break; 118 | escaped = !escaped && ch == "\\"; 119 | } 120 | if (!escaped) state.cur = normal; 121 | return "string"; 122 | }; 123 | } 124 | 125 | return { 126 | startState: function(basecol) { 127 | return {basecol: basecol || 0, indentDepth: 0, cur: normal}; 128 | }, 129 | 130 | token: function(stream, state) { 131 | if (stream.eatSpace()) return null; 132 | var style = state.cur(stream, state); 133 | var word = stream.current(); 134 | if (style == "variable") { 135 | if (keywords.test(word)) style = "keyword"; 136 | else if (builtins.test(word)) style = "builtin"; 137 | else if (specials.test(word)) style = "variable-2"; 138 | } 139 | if ((style != "comment") && (style != "string")){ 140 | if (indentTokens.test(word)) ++state.indentDepth; 141 | else if (dedentTokens.test(word)) --state.indentDepth; 142 | } 143 | return style; 144 | }, 145 | 146 | indent: function(state, textAfter) { 147 | var closing = dedentPartial.test(textAfter); 148 | return state.basecol + indentUnit * (state.indentDepth - (closing ? 1 : 0)); 149 | }, 150 | 151 | lineComment: "--", 152 | blockCommentStart: "--[[", 153 | blockCommentEnd: "]]" 154 | }; 155 | }); 156 | 157 | CodeMirror.defineMIME("text/x-lua", "lua"); 158 | 159 | }); 160 | -------------------------------------------------------------------------------- /mod_ws_api/data/ws/ws.lua: -------------------------------------------------------------------------------- 1 | dofile("data/ws/pollws.lua") 2 | dofile("data/ws/host.lua") 3 | dofile("data/ws/coroutines.lua") 4 | 5 | -- this empty table is used as a special value that will suppress 6 | -- printing any kind of "RES>" value (normally "[no value]" would print) 7 | local UNPRINTABLE_RESULT = {} 8 | 9 | -- (from http://lua-users.org/wiki/SplitJoin) 10 | local strfind = string.find 11 | local tinsert = table.insert 12 | local strsub = string.sub 13 | local function strsplit(text, delimiter) 14 | local list = {} 15 | local pos = 1 16 | if strfind("", delimiter, 1) then -- this would result in endless loops 17 | error("Delimiter matches empty string!") 18 | end 19 | while 1 do 20 | local first, last = strfind(text, delimiter, pos) 21 | if first then -- found? 22 | tinsert(list, strsub(text, pos, first-1)) 23 | pos = last+1 24 | else 25 | tinsert(list, strsub(text, pos)) 26 | break 27 | end 28 | end 29 | return list 30 | end 31 | 32 | local main_socket = nil 33 | 34 | local function reconnect(url) 35 | url = url or get_ws_host_url() -- comes from data/ws/host.lua 36 | if not url then return false end 37 | GamePrint("ws-api: trying to connect to " .. url) 38 | main_socket = open_socket(url) 39 | main_socket:poll() 40 | end 41 | 42 | local printing_to_socket = false 43 | 44 | local function set_scratch_size(scratch_size) 45 | if not scratch_size then error("Scratch size cannot be nil") end 46 | main_socket:set_scratch_size(scratch_size) 47 | end 48 | 49 | local function socket_send(msg) 50 | main_socket:send(msg) 51 | end 52 | 53 | local function socket_print(str) 54 | local msg = ">" .. str 55 | main_socket:send(msg) 56 | end 57 | 58 | function cprint(...) 59 | local m = table.concat({...}, " ") 60 | if main_socket and printing_to_socket then 61 | socket_print(m) 62 | else 63 | print(m) 64 | end 65 | end 66 | 67 | local function set_print_to_socket(p) 68 | printing_to_socket = p 69 | end 70 | 71 | local function cprint_table(t) 72 | local s = {} 73 | for k, v in pairs(t) do 74 | table.insert(s, k .. ": " .. type(v)) 75 | end 76 | cprint(table.concat(s, "\n")) 77 | end 78 | 79 | local console_env = nil 80 | 81 | local function reload_utils() 82 | local env_utils, err = loadfile("data/ws/utils.lua") 83 | if type(env_utils) ~= "function" then 84 | cprint("Error loading utils: " .. tostring(err)) 85 | return 86 | end 87 | setfenv(env_utils, console_env) 88 | local happy, err = pcall(env_utils) 89 | if not happy then 90 | cprint("Error executing utils: " .. tostring(err)) 91 | end 92 | cprint("Utils loaded.") 93 | end 94 | 95 | local _persistent_funcs = {} 96 | 97 | local function add_persistent_func(name, f) 98 | _persistent_funcs[name] = f 99 | end 100 | 101 | local function remove_persistent_func(name) 102 | _persistent_funcs[name] = nil 103 | end 104 | 105 | local function run_persistent_funcs() 106 | for fname, f in pairs(_persistent_funcs) do 107 | local happy, err = pcall(f, fname) 108 | if not happy then cprint(err) end 109 | end 110 | end 111 | 112 | local function _dofile(fn) 113 | local s = loadfile(fn) 114 | if type(s) == 'string' then 115 | -- work around Noita's broken loadfile that returns error 116 | -- message as first argument rather than as second 117 | error(fn .. ": " .. s) 118 | end 119 | setfenv(s, console_env) 120 | return s() 121 | end 122 | 123 | local _help_info = nil 124 | local function reload_help(fn) 125 | fn = fn or "tools_modding/lua_api_documentation.txt" 126 | local f, err = io.open(fn) 127 | if not f then error("Couldn't open " .. fn) end 128 | local res = f:read("*a") 129 | f:close() 130 | if not res then error("Couldn't read " .. fn) end 131 | _help_info = {} 132 | res = res:gsub("\r", "") -- get rid of horrible carriage returns 133 | local lines = strsplit(res, "\n") 134 | for _, line in ipairs(lines) do 135 | local paren_idx = line:find("%(") 136 | if paren_idx then 137 | local funcname = line:sub(1, paren_idx-1) 138 | _help_info[funcname] = line 139 | end 140 | end 141 | end 142 | 143 | local function help_str(funcname) 144 | if not _help_info then reload_help() end 145 | return _help_info[funcname] 146 | end 147 | 148 | local function help(funcname) 149 | cprint(help_str(funcname) or (funcname .. "-> [no help available]")) 150 | return UNPRINTABLE_RESULT 151 | end 152 | 153 | local function _strinfo(v) 154 | if v == nil then return "nil" end 155 | local vtype = type(v) 156 | if vtype == "number" then 157 | return ("%0.4f"):format(v) 158 | elseif vtype == "string" then 159 | return '"' .. v .. '"' 160 | elseif vtype == "boolean" then 161 | return tostring(v) 162 | else 163 | return ("[%s] %s"):format(vtype, tostring(v)) 164 | end 165 | end 166 | 167 | local function strinfo(...) 168 | local frags = {} 169 | local nargs = select('#', ...) 170 | if nargs == 0 then 171 | return "[no value]" 172 | end 173 | if nargs == 1 and select(1, ...) == UNPRINTABLE_RESULT then 174 | return UNPRINTABLE_RESULT 175 | end 176 | for idx = 1, nargs do 177 | frags[idx] = _strinfo(select(idx, ...)) 178 | end 179 | return table.concat(frags, ", ") 180 | end 181 | 182 | local function info(...) 183 | cprint(strinfo(...)) 184 | return UNPRINTABLE_RESULT 185 | end 186 | 187 | local function complete(s) 188 | local opts = {} 189 | 190 | local parts = strsplit(s, "%.") -- strsplit takes a pattern, so have to escape "." 191 | local cur = console_env 192 | local prefix = "" 193 | for idx = 1, (#parts) - 1 do 194 | cur = cur[parts[idx]] 195 | if not cur then return UNPRINTABLE_RESULT end 196 | prefix = prefix .. parts[idx] .. "." 197 | end 198 | if type(cur) ~= "table" then return UNPRINTABLE_RESULT end 199 | local lastpart = parts[#parts] 200 | if not lastpart then return UNPRINTABLE_RESULT end 201 | for k, _ in pairs(cur) do 202 | if k:find(lastpart) == 1 then 203 | table.insert(opts, k) 204 | end 205 | end 206 | if #opts > 0 then 207 | table.sort(opts) 208 | cprint("COM>" .. prefix .. " " .. table.concat(opts, ",")) 209 | end 210 | return UNPRINTABLE_RESULT 211 | end 212 | 213 | local function new_console_env() 214 | console_env = {} 215 | for k, v in pairs(getfenv()) do 216 | console_env[k] = v 217 | end 218 | console_env.reconnect = reconnect 219 | console_env.complete = complete 220 | console_env.new_console_env = new_console_env 221 | console_env.set_print_to_socket = set_print_to_socket 222 | console_env.print = cprint 223 | console_env.print_table = cprint_table 224 | console_env.socket_print = socket_print 225 | console_env.socket_send = socket_send 226 | console_env.rawprint = print 227 | console_env.reload_utils = reload_utils 228 | console_env.add_persistent_func = add_persistent_func 229 | console_env.set_persistent_func = add_persistent_func -- alias 230 | console_env.remove_persistent_func = remove_persistent_func 231 | console_env.strinfo = strinfo 232 | console_env.info = info 233 | console_env.dofile = _dofile 234 | console_env.reload_help = reload_help 235 | console_env.help_str = help_str 236 | console_env.help = help 237 | console_env.UNPRINTABLE_RESULT = UNPRINTABLE_RESULT 238 | 239 | reload_utils() 240 | end 241 | new_console_env() 242 | 243 | local function _collect(happy, ...) 244 | if happy then 245 | return happy, strinfo(...) 246 | else 247 | return happy, ... 248 | end 249 | end 250 | 251 | local function do_command(msg) 252 | local f, err = nil, nil 253 | if not msg:find("\n") then 254 | -- if this is a single line, try putting "return " in front 255 | -- (convenience to allow us to inspect values) 256 | f, err = loadstring("return " .. msg) 257 | end 258 | if not f then -- multiline, or not an expression 259 | f, err = loadstring(msg) 260 | if not f then 261 | cprint("ERR> Parse error: " .. tostring(err)) 262 | return 263 | end 264 | end 265 | setfenv(f, console_env) 266 | local happy, retval = _collect(pcall(f)) 267 | if happy then 268 | if retval ~= UNPRINTABLE_RESULT then 269 | cprint("RES> " .. tostring(retval)) 270 | end 271 | else 272 | cprint("ERR> " .. tostring(retval)) 273 | end 274 | end 275 | 276 | local count = 0 277 | 278 | _ws_main = function() 279 | if main_socket then 280 | local msg = main_socket:poll() 281 | if msg then 282 | do_command(msg) 283 | end 284 | if count % 60 == 0 then 285 | main_socket:send('{"kind": "heartbeat", "source": "noita"}') 286 | end 287 | run_persistent_funcs() 288 | elseif count % 60 == 0 then 289 | reconnect() 290 | end 291 | wake_up_waiting_threads(1) -- from coroutines.lua 292 | count = count + 1 293 | end -------------------------------------------------------------------------------- /servers/twitch_fragments/outcomes.lua: -------------------------------------------------------------------------------- 1 | dofile("data/scripts/lib/utilities.lua") 2 | dofile("data/scripts/perks/perk.lua") 3 | dofile("data/scripts/perks/perk_list.lua") 4 | 5 | local function insert_constant(outcome) 6 | table.insert(outcome_generators, function() 7 | return outcome 8 | end) 9 | end 10 | 11 | 12 | table.insert(outcome_generators, function() 13 | -- extracted from data/scripts/items/potion.lua 14 | local materials = nil 15 | if( Random( 0, 100 ) <= 75 ) then 16 | if( Random( 0, 100000 ) <= 50 ) then 17 | potion_material = "magic_liquid_hp_regeneration" 18 | elseif( Random( 200, 100000 ) <= 250 ) then 19 | potion_material = "purifying_powder" 20 | else 21 | potion_material = random_from_array( potion_materials_magic ) 22 | end 23 | else 24 | potion_material = random_from_array( potion_materials_standard ) 25 | end 26 | 27 | local matname = GameTextGet("$mat_" .. potion_material) 28 | 29 | return { 30 | name = "Flask: " .. matname, 31 | desc = "Enjoy", 32 | func = function() 33 | local x, y = get_player_pos() 34 | -- just go ahead and assume cheatgui is installed 35 | local entity = EntityLoad( "data/hax/potion_empty.xml", x, y ) 36 | AddMaterialInventoryMaterial( entity, potion_material, 1000 ) 37 | end 38 | } 39 | end) 40 | 41 | local function resolve_localized_name(s, default) 42 | if s:sub(1,1) ~= "$" then return s end 43 | local rep = GameTextGet(s) 44 | if rep and rep ~= "" then return rep else return default or s end 45 | end 46 | 47 | table.insert(outcome_generators, function() 48 | -- spawn a random perk 49 | local perk = perk_list[math.random(1, #perk_list)] 50 | 51 | -- reroll useless perk 52 | while perk.id == "MYSTERY_EGGPLANT" do 53 | perk = perk_list[math.random(1, #perk_list)] 54 | end 55 | 56 | local perkname = resolve_localized_name(perk.ui_name, perk.id) 57 | return { 58 | name = "Perk: " .. perkname, 59 | desc = "Have fun", 60 | func = function() 61 | local x, y = get_player_pos() 62 | 63 | -- player might be dead 64 | if x ~= nil and y ~= nil then 65 | local perk_entity = perk_spawn( x, y - 8, perk.id ) 66 | local players = get_players() 67 | if players == nil then return end 68 | for i,player in ipairs(players) do 69 | -- last argument set to false, so you dont kill others perks if triggered inside the shop 70 | perk_pickup( perk_entity, player, nil, true, false ) 71 | end 72 | end 73 | end 74 | } 75 | end) 76 | 77 | local function twiddle_health(f) 78 | local damagemodels = EntityGetComponent( get_player(), "DamageModelComponent" ) 79 | if( damagemodels ~= nil ) then 80 | for i,damagemodel in ipairs(damagemodels) do 81 | local max_hp = tonumber(ComponentGetValue( damagemodel, "max_hp")) 82 | local cur_hp = tonumber(ComponentGetValue( damagemodel, "hp")) 83 | local new_cur, new_max = f(cur_hp, max_hp) 84 | ComponentSetValue( damagemodel, "max_hp", new_max) 85 | ComponentSetValue( damagemodel, "hp", new_cur) 86 | end 87 | end 88 | end 89 | 90 | insert_constant{ 91 | name = "Health Down", 92 | desc = "Unfortunate", 93 | func = function() 94 | twiddle_health(function(cur, max) 95 | max = max * 0.8 96 | cur = math.min(max, cur) 97 | return cur, max 98 | end) 99 | end 100 | } 101 | 102 | insert_constant{ 103 | name = "Health Up", 104 | desc = "Amazing", 105 | func = function() 106 | twiddle_health(function(cur, max) 107 | return cur+1, max+1 108 | end) 109 | end 110 | } 111 | 112 | local function urand(mag) 113 | return math.floor((math.random()*2.0 - 1.0)*mag) 114 | end 115 | 116 | local function spawn_item(path, offset_mag) 117 | local x, y = get_player_pos() 118 | local dx, dy = urand(offset_mag or 0), urand(offset_mag or 0) 119 | print(x + dx, y + dy) 120 | local entity = EntityLoad(path, x + dx, y + dy) 121 | end 122 | 123 | local function wrap_spawn(path, offset_mag) 124 | return function() spawn_item(path, offset_mag) end 125 | end 126 | 127 | -- 0 to not limit axis, -1 to limit to negative values, 1 to limit to positive values 128 | local function generate_value_in_range(max_range, min_range, limit_axis) 129 | local range = (max_range or 0) - (min_range or 0) 130 | if (limit_axis or 0) == 0 then 131 | limit_axis = Random(0, 1) == 0 and 1 or -1 132 | end 133 | 134 | return (Random(0, range) + (min_range or 0)) * limit_axis 135 | end 136 | 137 | local function spawn_item_in_range(path, min_x_range, max_x_range, min_y_range, max_y_range, limit_x_axis, limit_y_axis, spawn_blackhole) 138 | local x, y = get_player_pos() 139 | local dx = generate_value_in_range(max_x_range, min_x_range, limit_x_axis) 140 | local dy = generate_value_in_range(max_y_range, min_y_range, limit_y_axis) 141 | 142 | if spawn_blackhole then 143 | EntityLoad("data/entities/projectiles/deck/black_hole.xml", x + dx, y + dy) 144 | end 145 | 146 | return EntityLoad(path, x + dx, y + dy) 147 | end 148 | 149 | local function spawn_item(path, min_range, max_range, spawn_blackhole) 150 | return spawn_item_in_range(path, min_range, max_range, min_range, max_range, 0, 0, spawn_blackhole) 151 | end 152 | 153 | 154 | insert_constant{ 155 | name = "The Gods Are Angry", 156 | desc = "...", 157 | func = function() 158 | spawn_item("data/entities/animals/necromancer_shop.xml", 100, 100, true) 159 | end 160 | } 161 | 162 | insert_constant{ 163 | name = "A big worm", 164 | desc = "This could be a problem", 165 | func = function() 166 | spawn_item("data/entities/animals/worm_big.xml", 100, 100) 167 | end 168 | } 169 | 170 | insert_constant{ 171 | name = "The biggest worm", 172 | desc = "oh ... oh no ... OH NO NO NO", 173 | func = function() 174 | spawn_item("data/entities/animals/worm_end.xml", 100, 150) 175 | end 176 | } 177 | 178 | insert_constant{ 179 | name = "A couple of worms", 180 | desc = "That's annoying", 181 | func = function() 182 | spawn_item("data/entities/animals/worm.xml", 50, 200) 183 | spawn_item("data/entities/animals/worm.xml", 50, 200) 184 | end 185 | } 186 | 187 | insert_constant{ 188 | name = "A can of worms", 189 | desc = "But why?", 190 | func = function() 191 | for i=1,10 do 192 | spawn_item("data/entities/animals/worm_tiny.xml", 50, 200) 193 | end 194 | end 195 | } 196 | 197 | insert_constant{ 198 | name = "Deers", 199 | desc = "Oh dear!", 200 | func = function() 201 | for i=1,5 do 202 | spawn_item("data/entities/projectiles/deck/exploding_deer.xml", 100, 300) 203 | spawn_item("data/entities/animals/deer.xml", 100, 300) 204 | end 205 | end 206 | } 207 | 208 | 209 | insert_constant{ 210 | name = "Gold rush", 211 | desc = "Quick, before it disappear", 212 | func = function() 213 | for i=1,20 do 214 | spawn_item("data/entities/items/pickup/goldnugget.xml", 50, 200) 215 | end 216 | end 217 | } 218 | 219 | insert_constant{ 220 | name = "Bomb rush", 221 | desc = "You better run", 222 | func = function() 223 | for i=1,3 do 224 | spawn_item("data/entities/projectiles/bomb.xml", 0, 50) 225 | end 226 | end 227 | } 228 | 229 | insert_constant{ 230 | name = "Sea of lava", 231 | desc = "Now, that's hot!!", 232 | func = function() 233 | spawn_item_in_range("data/entities/projectiles/deck/sea_lava.xml", 0, 200, 20, 100, 0, 1, false) 234 | end 235 | } 236 | 237 | insert_constant{ 238 | name = "ACID??", 239 | desc = "Who thought this was a good idea", 240 | func = function() 241 | spawn_item("data/entities/projectiles/deck/circle_acid.xml", 40, 120, true) 242 | end 243 | } 244 | 245 | insert_constant{ 246 | name = "Holy bombastic", 247 | desc = "That should clear the path", 248 | func = function() 249 | spawn_item("data/entities/projectiles/bomb_holy.xml", 130, 250, true) 250 | end 251 | } 252 | 253 | insert_constant{ 254 | name = "Thunderstone madness", 255 | desc = "Careful now", 256 | func = function() 257 | for i=1,10 do 258 | spawn_item("data/entities/items/pickup/thunderstone.xml", 50, 250) 259 | end 260 | end 261 | } 262 | 263 | insert_constant{ 264 | name = "Instant swimming pool", 265 | desc = "Don't forget your swimsuit", 266 | func = function() 267 | spawn_item_in_range("data/entities/projectiles/deck/sea_water.xml", 0, 0, 40, 80, 0, -1, false) 268 | end 269 | } 270 | 271 | insert_constant{ 272 | name = "Random wand generator", 273 | desc = "Make good use of it", 274 | func = function() 275 | local rnd = Random(0, 1000) 276 | if rnd < 200 then 277 | spawn_item("data/entities/items/wand_level_01.xml") 278 | elseif rnd < 600 then 279 | spawn_item("data/entities/items/wand_level_02.xml") 280 | elseif rnd < 850 then 281 | spawn_item("data/entities/items/wand_level_03.xml") 282 | elseif rnd < 998 then 283 | spawn_item("data/entities/items/wand_level_04.xml") 284 | else 285 | spawn_item("data/entities/items/wand_level_05.xml") 286 | end 287 | end 288 | } 289 | -------------------------------------------------------------------------------- /servers/www_console/lib/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 100%; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre.CodeMirror-line, 17 | .CodeMirror pre.CodeMirror-line-like { 18 | padding: 0 4px; /* Horizontal padding of content */ 19 | } 20 | 21 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 22 | background-color: white; /* The little square between H and V scrollbars */ 23 | } 24 | 25 | /* GUTTER */ 26 | 27 | .CodeMirror-gutters { 28 | border-right: 1px solid #ddd; 29 | background-color: #f7f7f7; 30 | white-space: nowrap; 31 | } 32 | .CodeMirror-linenumbers {} 33 | .CodeMirror-linenumber { 34 | padding: 0 3px 0 5px; 35 | min-width: 20px; 36 | text-align: right; 37 | color: #999; 38 | white-space: nowrap; 39 | } 40 | 41 | .CodeMirror-guttermarker { color: black; } 42 | .CodeMirror-guttermarker-subtle { color: #999; } 43 | 44 | /* CURSOR */ 45 | 46 | .CodeMirror-cursor { 47 | border-left: 1px solid black; 48 | border-right: none; 49 | width: 0; 50 | } 51 | /* Shown when moving in bi-directional text */ 52 | .CodeMirror div.CodeMirror-secondarycursor { 53 | border-left: 1px solid silver; 54 | } 55 | .cm-fat-cursor .CodeMirror-cursor { 56 | width: auto; 57 | border: 0 !important; 58 | background: #7e7; 59 | } 60 | .cm-fat-cursor div.CodeMirror-cursors { 61 | z-index: 1; 62 | } 63 | .cm-fat-cursor-mark { 64 | background-color: rgba(20, 255, 20, 0.5); 65 | -webkit-animation: blink 1.06s steps(1) infinite; 66 | -moz-animation: blink 1.06s steps(1) infinite; 67 | animation: blink 1.06s steps(1) infinite; 68 | } 69 | .cm-animate-fat-cursor { 70 | width: auto; 71 | border: 0; 72 | -webkit-animation: blink 1.06s steps(1) infinite; 73 | -moz-animation: blink 1.06s steps(1) infinite; 74 | animation: blink 1.06s steps(1) infinite; 75 | background-color: #7e7; 76 | } 77 | @-moz-keyframes blink { 78 | 0% {} 79 | 50% { background-color: transparent; } 80 | 100% {} 81 | } 82 | @-webkit-keyframes blink { 83 | 0% {} 84 | 50% { background-color: transparent; } 85 | 100% {} 86 | } 87 | @keyframes blink { 88 | 0% {} 89 | 50% { background-color: transparent; } 90 | 100% {} 91 | } 92 | 93 | /* Can style cursor different in overwrite (non-insert) mode */ 94 | .CodeMirror-overwrite .CodeMirror-cursor {} 95 | 96 | .cm-tab { display: inline-block; text-decoration: inherit; } 97 | 98 | .CodeMirror-rulers { 99 | position: absolute; 100 | left: 0; right: 0; top: -50px; bottom: 0; 101 | overflow: hidden; 102 | } 103 | .CodeMirror-ruler { 104 | border-left: 1px solid #ccc; 105 | top: 0; bottom: 0; 106 | position: absolute; 107 | } 108 | 109 | /* DEFAULT THEME */ 110 | 111 | .cm-s-default .cm-header {color: blue;} 112 | .cm-s-default .cm-quote {color: #090;} 113 | .cm-negative {color: #d44;} 114 | .cm-positive {color: #292;} 115 | .cm-header, .cm-strong {font-weight: bold;} 116 | .cm-em {font-style: italic;} 117 | .cm-link {text-decoration: underline;} 118 | .cm-strikethrough {text-decoration: line-through;} 119 | 120 | .cm-s-default .cm-keyword {color: #708;} 121 | .cm-s-default .cm-atom {color: #219;} 122 | .cm-s-default .cm-number {color: #164;} 123 | .cm-s-default .cm-def {color: #00f;} 124 | .cm-s-default .cm-variable, 125 | .cm-s-default .cm-punctuation, 126 | .cm-s-default .cm-property, 127 | .cm-s-default .cm-operator {} 128 | .cm-s-default .cm-variable-2 {color: #05a;} 129 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 130 | .cm-s-default .cm-comment {color: #a50;} 131 | .cm-s-default .cm-string {color: #a11;} 132 | .cm-s-default .cm-string-2 {color: #f50;} 133 | .cm-s-default .cm-meta {color: #555;} 134 | .cm-s-default .cm-qualifier {color: #555;} 135 | .cm-s-default .cm-builtin {color: #30a;} 136 | .cm-s-default .cm-bracket {color: #997;} 137 | .cm-s-default .cm-tag {color: #170;} 138 | .cm-s-default .cm-attribute {color: #00c;} 139 | .cm-s-default .cm-hr {color: #999;} 140 | .cm-s-default .cm-link {color: #00c;} 141 | 142 | .cm-s-default .cm-error {color: #f00;} 143 | .cm-invalidchar {color: #f00;} 144 | 145 | .CodeMirror-composing { border-bottom: 2px solid; } 146 | 147 | /* Default styles for common addons */ 148 | 149 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 150 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 151 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 152 | .CodeMirror-activeline-background {background: #e8f2ff;} 153 | 154 | /* STOP */ 155 | 156 | /* The rest of this file contains styles related to the mechanics of 157 | the editor. You probably shouldn't touch them. */ 158 | 159 | .CodeMirror { 160 | position: relative; 161 | overflow: hidden; 162 | background: white; 163 | } 164 | 165 | .CodeMirror-scroll { 166 | overflow: scroll !important; /* Things will break if this is overridden */ 167 | /* 30px is the magic margin used to hide the element's real scrollbars */ 168 | /* See overflow: hidden in .CodeMirror */ 169 | margin-bottom: -30px; margin-right: -30px; 170 | padding-bottom: 30px; 171 | height: 100%; 172 | outline: none; /* Prevent dragging from highlighting the element */ 173 | position: relative; 174 | } 175 | .CodeMirror-sizer { 176 | position: relative; 177 | border-right: 30px solid transparent; 178 | } 179 | 180 | /* The fake, visible scrollbars. Used to force redraw during scrolling 181 | before actual scrolling happens, thus preventing shaking and 182 | flickering artifacts. */ 183 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 184 | position: absolute; 185 | z-index: 6; 186 | display: none; 187 | } 188 | .CodeMirror-vscrollbar { 189 | right: 0; top: 0; 190 | overflow-x: hidden; 191 | overflow-y: scroll; 192 | } 193 | .CodeMirror-hscrollbar { 194 | bottom: 0; left: 0; 195 | overflow-y: hidden; 196 | overflow-x: scroll; 197 | } 198 | .CodeMirror-scrollbar-filler { 199 | right: 0; bottom: 0; 200 | } 201 | .CodeMirror-gutter-filler { 202 | left: 0; bottom: 0; 203 | } 204 | 205 | .CodeMirror-gutters { 206 | position: absolute; left: 0; top: 0; 207 | min-height: 100%; 208 | z-index: 3; 209 | } 210 | .CodeMirror-gutter { 211 | white-space: normal; 212 | height: 100%; 213 | display: inline-block; 214 | vertical-align: top; 215 | margin-bottom: -30px; 216 | } 217 | .CodeMirror-gutter-wrapper { 218 | position: absolute; 219 | z-index: 4; 220 | background: none !important; 221 | border: none !important; 222 | } 223 | .CodeMirror-gutter-background { 224 | position: absolute; 225 | top: 0; bottom: 0; 226 | z-index: 4; 227 | } 228 | .CodeMirror-gutter-elt { 229 | position: absolute; 230 | cursor: default; 231 | z-index: 4; 232 | } 233 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 234 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 235 | 236 | .CodeMirror-lines { 237 | cursor: text; 238 | min-height: 1px; /* prevents collapsing before first draw */ 239 | } 240 | .CodeMirror pre.CodeMirror-line, 241 | .CodeMirror pre.CodeMirror-line-like { 242 | /* Reset some styles that the rest of the page might have set */ 243 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 244 | border-width: 0; 245 | background: transparent; 246 | font-family: inherit; 247 | font-size: inherit; 248 | margin: 0; 249 | white-space: pre; 250 | word-wrap: normal; 251 | line-height: inherit; 252 | color: inherit; 253 | z-index: 2; 254 | position: relative; 255 | overflow: visible; 256 | -webkit-tap-highlight-color: transparent; 257 | -webkit-font-variant-ligatures: contextual; 258 | font-variant-ligatures: contextual; 259 | } 260 | .CodeMirror-wrap pre.CodeMirror-line, 261 | .CodeMirror-wrap pre.CodeMirror-line-like { 262 | word-wrap: break-word; 263 | white-space: pre-wrap; 264 | word-break: normal; 265 | } 266 | 267 | .CodeMirror-linebackground { 268 | position: absolute; 269 | left: 0; right: 0; top: 0; bottom: 0; 270 | z-index: 0; 271 | } 272 | 273 | .CodeMirror-linewidget { 274 | position: relative; 275 | z-index: 2; 276 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 277 | } 278 | 279 | .CodeMirror-widget {} 280 | 281 | .CodeMirror-rtl pre { direction: rtl; } 282 | 283 | .CodeMirror-code { 284 | outline: none; 285 | } 286 | 287 | /* Force content-box sizing for the elements where we expect it */ 288 | .CodeMirror-scroll, 289 | .CodeMirror-sizer, 290 | .CodeMirror-gutter, 291 | .CodeMirror-gutters, 292 | .CodeMirror-linenumber { 293 | -moz-box-sizing: content-box; 294 | box-sizing: content-box; 295 | } 296 | 297 | .CodeMirror-measure { 298 | position: absolute; 299 | width: 100%; 300 | height: 0; 301 | overflow: hidden; 302 | visibility: hidden; 303 | } 304 | 305 | .CodeMirror-cursor { 306 | position: absolute; 307 | pointer-events: none; 308 | } 309 | .CodeMirror-measure pre { position: static; } 310 | 311 | div.CodeMirror-cursors { 312 | visibility: hidden; 313 | position: relative; 314 | z-index: 3; 315 | } 316 | div.CodeMirror-dragcursors { 317 | visibility: visible; 318 | } 319 | 320 | .CodeMirror-focused div.CodeMirror-cursors { 321 | visibility: visible; 322 | } 323 | 324 | .CodeMirror-selected { background: #d9d9d9; } 325 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 326 | .CodeMirror-crosshair { cursor: crosshair; } 327 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 328 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 329 | 330 | .cm-searching { 331 | background-color: #ffa; 332 | background-color: rgba(255, 255, 0, .4); 333 | } 334 | 335 | /* Used to force a border model for a node */ 336 | .cm-force-border { padding-right: .1px; } 337 | 338 | @media print { 339 | /* Hide the cursor when printing */ 340 | .CodeMirror div.CodeMirror-cursors { 341 | visibility: hidden; 342 | } 343 | } 344 | 345 | /* See issue #2901 */ 346 | .cm-tab-wrap-hack:after { content: ''; } 347 | 348 | /* Help users use markselection to safely style text background */ 349 | span.CodeMirror-selectedtext { background: none; } 350 | -------------------------------------------------------------------------------- /servers/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noita-ws-example-servers", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@arr/every": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", 10 | "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==" 11 | }, 12 | "@polka/url": { 13 | "version": "0.5.0", 14 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz", 15 | "integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==" 16 | }, 17 | "ajv": { 18 | "version": "6.10.2", 19 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 20 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 21 | "requires": { 22 | "fast-deep-equal": "^2.0.1", 23 | "fast-json-stable-stringify": "^2.0.0", 24 | "json-schema-traverse": "^0.4.1", 25 | "uri-js": "^4.2.2" 26 | } 27 | }, 28 | "asn1": { 29 | "version": "0.2.4", 30 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 31 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 32 | "requires": { 33 | "safer-buffer": "~2.1.0" 34 | } 35 | }, 36 | "assert-plus": { 37 | "version": "1.0.0", 38 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 39 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 40 | }, 41 | "async-limiter": { 42 | "version": "1.0.1", 43 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 44 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 45 | }, 46 | "asynckit": { 47 | "version": "0.4.0", 48 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 49 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 50 | }, 51 | "aws-sign2": { 52 | "version": "0.7.0", 53 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 54 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 55 | }, 56 | "aws4": { 57 | "version": "1.8.0", 58 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 59 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 60 | }, 61 | "bcrypt-pbkdf": { 62 | "version": "1.0.2", 63 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 64 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 65 | "requires": { 66 | "tweetnacl": "^0.14.3" 67 | } 68 | }, 69 | "caseless": { 70 | "version": "0.12.0", 71 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 72 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 73 | }, 74 | "combined-stream": { 75 | "version": "1.0.8", 76 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 77 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 78 | "requires": { 79 | "delayed-stream": "~1.0.0" 80 | } 81 | }, 82 | "core-util-is": { 83 | "version": "1.0.2", 84 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 85 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 86 | }, 87 | "dashdash": { 88 | "version": "1.14.1", 89 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 90 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 91 | "requires": { 92 | "assert-plus": "^1.0.0" 93 | } 94 | }, 95 | "debug": { 96 | "version": "2.6.9", 97 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 98 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 99 | "requires": { 100 | "ms": "2.0.0" 101 | }, 102 | "dependencies": { 103 | "ms": { 104 | "version": "2.0.0", 105 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 106 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 107 | } 108 | } 109 | }, 110 | "delayed-stream": { 111 | "version": "1.0.0", 112 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 113 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 114 | }, 115 | "depd": { 116 | "version": "1.1.2", 117 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 118 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 119 | }, 120 | "destroy": { 121 | "version": "1.0.4", 122 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 123 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 124 | }, 125 | "dotenv": { 126 | "version": "5.0.1", 127 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", 128 | "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" 129 | }, 130 | "ecc-jsbn": { 131 | "version": "0.1.2", 132 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 133 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 134 | "requires": { 135 | "jsbn": "~0.1.0", 136 | "safer-buffer": "^2.1.0" 137 | } 138 | }, 139 | "ee-first": { 140 | "version": "1.1.1", 141 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 142 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 143 | }, 144 | "encodeurl": { 145 | "version": "1.0.2", 146 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 147 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 148 | }, 149 | "escape-html": { 150 | "version": "1.0.3", 151 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 152 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 153 | }, 154 | "etag": { 155 | "version": "1.8.1", 156 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 157 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 158 | }, 159 | "extend": { 160 | "version": "3.0.2", 161 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 162 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 163 | }, 164 | "extsprintf": { 165 | "version": "1.3.0", 166 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 167 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 168 | }, 169 | "fast-deep-equal": { 170 | "version": "2.0.1", 171 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 172 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 173 | }, 174 | "fast-json-stable-stringify": { 175 | "version": "2.0.0", 176 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 177 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 178 | }, 179 | "forever-agent": { 180 | "version": "0.6.1", 181 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 182 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 183 | }, 184 | "form-data": { 185 | "version": "2.3.3", 186 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 187 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 188 | "requires": { 189 | "asynckit": "^0.4.0", 190 | "combined-stream": "^1.0.6", 191 | "mime-types": "^2.1.12" 192 | } 193 | }, 194 | "fresh": { 195 | "version": "0.5.2", 196 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 197 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 198 | }, 199 | "getpass": { 200 | "version": "0.1.7", 201 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 202 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 203 | "requires": { 204 | "assert-plus": "^1.0.0" 205 | } 206 | }, 207 | "har-schema": { 208 | "version": "2.0.0", 209 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 210 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 211 | }, 212 | "har-validator": { 213 | "version": "5.1.3", 214 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 215 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 216 | "requires": { 217 | "ajv": "^6.5.5", 218 | "har-schema": "^2.0.0" 219 | } 220 | }, 221 | "http-errors": { 222 | "version": "1.7.3", 223 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", 224 | "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", 225 | "requires": { 226 | "depd": "~1.1.2", 227 | "inherits": "2.0.4", 228 | "setprototypeof": "1.1.1", 229 | "statuses": ">= 1.5.0 < 2", 230 | "toidentifier": "1.0.0" 231 | } 232 | }, 233 | "http-signature": { 234 | "version": "1.2.0", 235 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 236 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 237 | "requires": { 238 | "assert-plus": "^1.0.0", 239 | "jsprim": "^1.2.2", 240 | "sshpk": "^1.7.0" 241 | } 242 | }, 243 | "inherits": { 244 | "version": "2.0.4", 245 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 246 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 247 | }, 248 | "is-typedarray": { 249 | "version": "1.0.0", 250 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 251 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 252 | }, 253 | "is-wsl": { 254 | "version": "2.1.1", 255 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", 256 | "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==" 257 | }, 258 | "isstream": { 259 | "version": "0.1.2", 260 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 261 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 262 | }, 263 | "jsbn": { 264 | "version": "0.1.1", 265 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 266 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 267 | }, 268 | "json-schema": { 269 | "version": "0.2.3", 270 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 271 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 272 | }, 273 | "json-schema-traverse": { 274 | "version": "0.4.1", 275 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 276 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 277 | }, 278 | "json-stringify-safe": { 279 | "version": "5.0.1", 280 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 281 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 282 | }, 283 | "jsprim": { 284 | "version": "1.4.1", 285 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 286 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 287 | "requires": { 288 | "assert-plus": "1.0.0", 289 | "extsprintf": "1.3.0", 290 | "json-schema": "0.2.3", 291 | "verror": "1.10.0" 292 | } 293 | }, 294 | "matchit": { 295 | "version": "1.0.8", 296 | "resolved": "https://registry.npmjs.org/matchit/-/matchit-1.0.8.tgz", 297 | "integrity": "sha512-CwPPICzozd/ezCzpVwGYG5bMVieaapnA0vvHDQnmQ2u2vZtVLynoPmvFsZjL67hFOvTBhhpqSR0bq3uloDP/Rw==", 298 | "requires": { 299 | "@arr/every": "^1.0.0" 300 | } 301 | }, 302 | "mime": { 303 | "version": "1.6.0", 304 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 305 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 306 | }, 307 | "mime-db": { 308 | "version": "1.40.0", 309 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 310 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 311 | }, 312 | "mime-types": { 313 | "version": "2.1.24", 314 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 315 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 316 | "requires": { 317 | "mime-db": "1.40.0" 318 | } 319 | }, 320 | "ms": { 321 | "version": "2.1.1", 322 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 323 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 324 | }, 325 | "oauth-sign": { 326 | "version": "0.9.0", 327 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 328 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 329 | }, 330 | "on-finished": { 331 | "version": "2.3.0", 332 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 333 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 334 | "requires": { 335 | "ee-first": "1.1.1" 336 | } 337 | }, 338 | "open": { 339 | "version": "7.0.0", 340 | "resolved": "https://registry.npmjs.org/open/-/open-7.0.0.tgz", 341 | "integrity": "sha512-K6EKzYqnwQzk+/dzJAQSBORub3xlBTxMz+ntpZpH/LyCa1o6KjXhuN+2npAaI9jaSmU3R1Q8NWf4KUWcyytGsQ==", 342 | "requires": { 343 | "is-wsl": "^2.1.0" 344 | } 345 | }, 346 | "parseurl": { 347 | "version": "1.3.3", 348 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 349 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 350 | }, 351 | "performance-now": { 352 | "version": "2.1.0", 353 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 354 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 355 | }, 356 | "polka": { 357 | "version": "0.5.2", 358 | "resolved": "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz", 359 | "integrity": "sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==", 360 | "requires": { 361 | "@polka/url": "^0.5.0", 362 | "trouter": "^2.0.1" 363 | } 364 | }, 365 | "psl": { 366 | "version": "1.4.0", 367 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", 368 | "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" 369 | }, 370 | "punycode": { 371 | "version": "2.1.1", 372 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 373 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 374 | }, 375 | "qs": { 376 | "version": "6.5.2", 377 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 378 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 379 | }, 380 | "range-parser": { 381 | "version": "1.2.1", 382 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 383 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 384 | }, 385 | "request": { 386 | "version": "2.88.0", 387 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 388 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 389 | "requires": { 390 | "aws-sign2": "~0.7.0", 391 | "aws4": "^1.8.0", 392 | "caseless": "~0.12.0", 393 | "combined-stream": "~1.0.6", 394 | "extend": "~3.0.2", 395 | "forever-agent": "~0.6.1", 396 | "form-data": "~2.3.2", 397 | "har-validator": "~5.1.0", 398 | "http-signature": "~1.2.0", 399 | "is-typedarray": "~1.0.0", 400 | "isstream": "~0.1.2", 401 | "json-stringify-safe": "~5.0.1", 402 | "mime-types": "~2.1.19", 403 | "oauth-sign": "~0.9.0", 404 | "performance-now": "^2.1.0", 405 | "qs": "~6.5.2", 406 | "safe-buffer": "^5.1.2", 407 | "tough-cookie": "~2.4.3", 408 | "tunnel-agent": "^0.6.0", 409 | "uuid": "^3.3.2" 410 | } 411 | }, 412 | "safe-buffer": { 413 | "version": "5.2.0", 414 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 415 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 416 | }, 417 | "safer-buffer": { 418 | "version": "2.1.2", 419 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 420 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 421 | }, 422 | "send": { 423 | "version": "0.17.1", 424 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 425 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 426 | "requires": { 427 | "debug": "2.6.9", 428 | "depd": "~1.1.2", 429 | "destroy": "~1.0.4", 430 | "encodeurl": "~1.0.2", 431 | "escape-html": "~1.0.3", 432 | "etag": "~1.8.1", 433 | "fresh": "0.5.2", 434 | "http-errors": "~1.7.2", 435 | "mime": "1.6.0", 436 | "ms": "2.1.1", 437 | "on-finished": "~2.3.0", 438 | "range-parser": "~1.2.1", 439 | "statuses": "~1.5.0" 440 | } 441 | }, 442 | "serve-static": { 443 | "version": "1.14.1", 444 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 445 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 446 | "requires": { 447 | "encodeurl": "~1.0.2", 448 | "escape-html": "~1.0.3", 449 | "parseurl": "~1.3.3", 450 | "send": "0.17.1" 451 | } 452 | }, 453 | "setprototypeof": { 454 | "version": "1.1.1", 455 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 456 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 457 | }, 458 | "sshpk": { 459 | "version": "1.16.1", 460 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 461 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 462 | "requires": { 463 | "asn1": "~0.2.3", 464 | "assert-plus": "^1.0.0", 465 | "bcrypt-pbkdf": "^1.0.0", 466 | "dashdash": "^1.12.0", 467 | "ecc-jsbn": "~0.1.1", 468 | "getpass": "^0.1.1", 469 | "jsbn": "~0.1.0", 470 | "safer-buffer": "^2.0.2", 471 | "tweetnacl": "~0.14.0" 472 | } 473 | }, 474 | "statuses": { 475 | "version": "1.5.0", 476 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 477 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 478 | }, 479 | "tmi.js": { 480 | "version": "1.5.0", 481 | "resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.5.0.tgz", 482 | "integrity": "sha512-JyWKy9dRkZDG1h6PnpE8fJVsTrW82/yANXoP7R3u02vG7PLCvHGRGTWzBwk0ymMJGX9A+YzDx5tXQDsTeJd/5A==", 483 | "requires": { 484 | "request": "2.88.0", 485 | "ws": "6.1.3" 486 | }, 487 | "dependencies": { 488 | "ws": { 489 | "version": "6.1.3", 490 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", 491 | "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", 492 | "requires": { 493 | "async-limiter": "~1.0.0" 494 | } 495 | } 496 | } 497 | }, 498 | "toidentifier": { 499 | "version": "1.0.0", 500 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 501 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 502 | }, 503 | "tough-cookie": { 504 | "version": "2.4.3", 505 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 506 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 507 | "requires": { 508 | "psl": "^1.1.24", 509 | "punycode": "^1.4.1" 510 | }, 511 | "dependencies": { 512 | "punycode": { 513 | "version": "1.4.1", 514 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 515 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 516 | } 517 | } 518 | }, 519 | "trouter": { 520 | "version": "2.0.1", 521 | "resolved": "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz", 522 | "integrity": "sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==", 523 | "requires": { 524 | "matchit": "^1.0.0" 525 | } 526 | }, 527 | "tunnel-agent": { 528 | "version": "0.6.0", 529 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 530 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 531 | "requires": { 532 | "safe-buffer": "^5.0.1" 533 | } 534 | }, 535 | "tweetnacl": { 536 | "version": "0.14.5", 537 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 538 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 539 | }, 540 | "uri-js": { 541 | "version": "4.2.2", 542 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 543 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 544 | "requires": { 545 | "punycode": "^2.1.0" 546 | } 547 | }, 548 | "uuid": { 549 | "version": "3.3.3", 550 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", 551 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" 552 | }, 553 | "verror": { 554 | "version": "1.10.0", 555 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 556 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 557 | "requires": { 558 | "assert-plus": "^1.0.0", 559 | "core-util-is": "1.0.2", 560 | "extsprintf": "^1.2.0" 561 | } 562 | }, 563 | "ws": { 564 | "version": "5.2.2", 565 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 566 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 567 | "requires": { 568 | "async-limiter": "~1.0.0" 569 | } 570 | }, 571 | "xterm": { 572 | "version": "4.2.0-vscode1", 573 | "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.2.0-vscode1.tgz", 574 | "integrity": "sha512-frw5Kprzkko8+G0vp4jOcwTeOpYFbSb2+QShpeQGJ7l3Hg3MTM7kt1G+nLxBPKnkFAmei/JcJTUGJdp/lldAfA==" 575 | }, 576 | "xterm-addon-fit": { 577 | "version": "0.3.0", 578 | "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.3.0.tgz", 579 | "integrity": "sha512-kvkiqHVrnMXgyCH9Xn0BOBJ7XaWC/4BgpSWQy3SueqximgW630t/QOankgqkvk11iTOCwWdAY9DTyQBXUMN3lw==" 580 | } 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /mod_ws_api/data/ws/json.lua: -------------------------------------------------------------------------------- 1 | -- This file is huge and Noita doesn't have real requires, so 2 | -- manually guard against multiple dofiles 3 | if JSON then return end 4 | 5 | -- -*- coding: utf-8 -*- 6 | -- 7 | -- Simple JSON encoding and decoding in pure Lua. 8 | -- 9 | -- Copyright 2010-2017 Jeffrey Friedl 10 | -- http://regex.info/blog/ 11 | -- Latest version: http://regex.info/blog/lua/json 12 | -- 13 | -- This code is released under a Creative Commons CC-BY "Attribution" License: 14 | -- http://creativecommons.org/licenses/by/3.0/deed.en_US 15 | -- 16 | -- It can be used for any purpose so long as: 17 | -- 1) the copyright notice above is maintained 18 | -- 2) the web-page links above are maintained 19 | -- 3) the 'AUTHOR_NOTE' string below is maintained 20 | -- 21 | local VERSION = '20170927.26' -- version history at end of file 22 | local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20170927.26 ]-" 23 | 24 | -- 25 | -- The 'AUTHOR_NOTE' variable exists so that information about the source 26 | -- of the package is maintained even in compiled versions. It's also 27 | -- included in OBJDEF below mostly to quiet warnings about unused variables. 28 | -- 29 | local OBJDEF = { 30 | VERSION = VERSION, 31 | AUTHOR_NOTE = AUTHOR_NOTE, 32 | } 33 | 34 | 35 | -- 36 | -- Simple JSON encoding and decoding in pure Lua. 37 | -- JSON definition: http://www.json.org/ 38 | -- 39 | -- 40 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 41 | -- 42 | -- local lua_value = JSON:decode(raw_json_text) 43 | -- 44 | -- local raw_json_text = JSON:encode(lua_table_or_value) 45 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 46 | -- 47 | -- 48 | -- 49 | -- DECODING (from a JSON string to a Lua table) 50 | -- 51 | -- 52 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 53 | -- 54 | -- local lua_value = JSON:decode(raw_json_text) 55 | -- 56 | -- If the JSON text is for an object or an array, e.g. 57 | -- { "what": "books", "count": 3 } 58 | -- or 59 | -- [ "Larry", "Curly", "Moe" ] 60 | -- 61 | -- the result is a Lua table, e.g. 62 | -- { what = "books", count = 3 } 63 | -- or 64 | -- { "Larry", "Curly", "Moe" } 65 | -- 66 | -- 67 | -- The encode and decode routines accept an optional second argument, 68 | -- "etc", which is not used during encoding or decoding, but upon error 69 | -- is passed along to error handlers. It can be of any type (including nil). 70 | -- 71 | -- 72 | -- 73 | -- ERROR HANDLING DURING DECODE 74 | -- 75 | -- With most errors during decoding, this code calls 76 | -- 77 | -- JSON:onDecodeError(message, text, location, etc) 78 | -- 79 | -- with a message about the error, and if known, the JSON text being 80 | -- parsed and the byte count where the problem was discovered. You can 81 | -- replace the default JSON:onDecodeError() with your own function. 82 | -- 83 | -- The default onDecodeError() merely augments the message with data 84 | -- about the text and the location (and, an 'etc' argument had been 85 | -- provided to decode(), its value is tacked onto the message as well), 86 | -- and then calls JSON.assert(), which itself defaults to Lua's built-in 87 | -- assert(), and can also be overridden. 88 | -- 89 | -- For example, in an Adobe Lightroom plugin, you might use something like 90 | -- 91 | -- function JSON:onDecodeError(message, text, location, etc) 92 | -- LrErrors.throwUserError("Internal Error: invalid JSON data") 93 | -- end 94 | -- 95 | -- or even just 96 | -- 97 | -- function JSON.assert(message) 98 | -- LrErrors.throwUserError("Internal Error: " .. message) 99 | -- end 100 | -- 101 | -- If JSON:decode() is passed a nil, this is called instead: 102 | -- 103 | -- JSON:onDecodeOfNilError(message, nil, nil, etc) 104 | -- 105 | -- and if JSON:decode() is passed HTML instead of JSON, this is called: 106 | -- 107 | -- JSON:onDecodeOfHTMLError(message, text, nil, etc) 108 | -- 109 | -- The use of the 'etc' argument allows stronger coordination between 110 | -- decoding and error reporting, especially when you provide your own 111 | -- error-handling routines. Continuing with the the Adobe Lightroom 112 | -- plugin example: 113 | -- 114 | -- function JSON:onDecodeError(message, text, location, etc) 115 | -- local note = "Internal Error: invalid JSON data" 116 | -- if type(etc) = 'table' and etc.photo then 117 | -- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') 118 | -- end 119 | -- LrErrors.throwUserError(note) 120 | -- end 121 | -- 122 | -- : 123 | -- : 124 | -- 125 | -- for i, photo in ipairs(photosToProcess) do 126 | -- : 127 | -- : 128 | -- local data = JSON:decode(someJsonText, { photo = photo }) 129 | -- : 130 | -- : 131 | -- end 132 | -- 133 | -- 134 | -- 135 | -- If the JSON text passed to decode() has trailing garbage (e.g. as with the JSON "[123]xyzzy"), 136 | -- the method 137 | -- 138 | -- JSON:onTrailingGarbage(json_text, location, parsed_value, etc) 139 | -- 140 | -- is invoked, where: 141 | -- 142 | -- 'json_text' is the original JSON text being parsed, 143 | -- 'location' is the count of bytes into 'json_text' where the garbage starts (6 in the example), 144 | -- 'parsed_value' is the Lua result of what was successfully parsed ({123} in the example), 145 | -- 'etc' is as above. 146 | -- 147 | -- If JSON:onTrailingGarbage() does not abort, it should return the value decode() should return, 148 | -- or nil + an error message. 149 | -- 150 | -- local new_value, error_message = JSON:onTrailingGarbage() 151 | -- 152 | -- The default JSON:onTrailingGarbage() simply invokes JSON:onDecodeError("trailing garbage"...), 153 | -- but you can have this package ignore trailing garbage via 154 | -- 155 | -- function JSON:onTrailingGarbage(json_text, location, parsed_value, etc) 156 | -- return parsed_value 157 | -- end 158 | -- 159 | -- 160 | -- DECODING AND STRICT TYPES 161 | -- 162 | -- Because both JSON objects and JSON arrays are converted to Lua tables, 163 | -- it's not normally possible to tell which original JSON type a 164 | -- particular Lua table was derived from, or guarantee decode-encode 165 | -- round-trip equivalency. 166 | -- 167 | -- However, if you enable strictTypes, e.g. 168 | -- 169 | -- JSON = assert(loadfile "JSON.lua")() --load the routines 170 | -- JSON.strictTypes = true 171 | -- 172 | -- then the Lua table resulting from the decoding of a JSON object or 173 | -- JSON array is marked via Lua metatable, so that when re-encoded with 174 | -- JSON:encode() it ends up as the appropriate JSON type. 175 | -- 176 | -- (This is not the default because other routines may not work well with 177 | -- tables that have a metatable set, for example, Lightroom API calls.) 178 | -- 179 | -- 180 | -- ENCODING (from a lua table to a JSON string) 181 | -- 182 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 183 | -- 184 | -- local raw_json_text = JSON:encode(lua_table_or_value) 185 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 186 | -- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) 187 | -- 188 | -- On error during encoding, this code calls: 189 | -- 190 | -- JSON:onEncodeError(message, etc) 191 | -- 192 | -- which you can override in your local JSON object. Also see "HANDLING UNSUPPORTED VALUE TYPES" below. 193 | -- 194 | -- The 'etc' in the error call is the second argument to encode() and encode_pretty(), or nil if it wasn't provided. 195 | -- 196 | -- 197 | -- 198 | -- 199 | -- ENCODING OPTIONS 200 | -- 201 | -- An optional third argument, a table of options, can be provided to encode(). 202 | -- 203 | -- encode_options = { 204 | -- -- options for making "pretty" human-readable JSON (see "PRETTY-PRINTING" below) 205 | -- pretty = true, -- turn pretty formatting on 206 | -- indent = " ", -- use this indent for each level of an array/object 207 | -- align_keys = false, -- if true, align the keys in a way that sounds like it should be nice, but is actually ugly 208 | -- array_newline = false, -- if true, array elements become one to a line rather than inline 209 | -- 210 | -- -- other output-related options 211 | -- null = "\0", -- see "ENCODING JSON NULL VALUES" below 212 | -- stringsAreUtf8 = false, -- see "HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA" below 213 | -- } 214 | -- 215 | -- json_string = JSON:encode(mytable, etc, encode_options) 216 | -- 217 | -- 218 | -- 219 | -- For reference, the defaults are: 220 | -- 221 | -- pretty = false 222 | -- null = nil, 223 | -- stringsAreUtf8 = false, 224 | -- 225 | -- 226 | -- 227 | -- PRETTY-PRINTING 228 | -- 229 | -- Enabling the 'pretty' encode option helps generate human-readable JSON. 230 | -- 231 | -- pretty = JSON:encode(val, etc, { 232 | -- pretty = true, 233 | -- indent = " ", 234 | -- align_keys = false, 235 | -- }) 236 | -- 237 | -- encode_pretty() is also provided: it's identical to encode() except 238 | -- that encode_pretty() provides a default options table if none given in the call: 239 | -- 240 | -- { pretty = true, indent = " ", align_keys = false, array_newline = false } 241 | -- 242 | -- For example, if 243 | -- 244 | -- JSON:encode(data) 245 | -- 246 | -- produces: 247 | -- 248 | -- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} 249 | -- 250 | -- then 251 | -- 252 | -- JSON:encode_pretty(data) 253 | -- 254 | -- produces: 255 | -- 256 | -- { 257 | -- "city": "Kyoto", 258 | -- "climate": { 259 | -- "avg_temp": 16, 260 | -- "humidity": "high", 261 | -- "snowfall": "minimal" 262 | -- }, 263 | -- "country": "Japan", 264 | -- "wards": 11 265 | -- } 266 | -- 267 | -- The following lines all return identical strings: 268 | -- JSON:encode_pretty(data) 269 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = false, array_newline = false}) 270 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " " }) 271 | -- JSON:encode (data, nil, { pretty = true, indent = " " }) 272 | -- 273 | -- An example of setting your own indent string: 274 | -- 275 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) 276 | -- 277 | -- produces: 278 | -- 279 | -- { 280 | -- | "city": "Kyoto", 281 | -- | "climate": { 282 | -- | | "avg_temp": 16, 283 | -- | | "humidity": "high", 284 | -- | | "snowfall": "minimal" 285 | -- | }, 286 | -- | "country": "Japan", 287 | -- | "wards": 11 288 | -- } 289 | -- 290 | -- An example of setting align_keys to true: 291 | -- 292 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) 293 | -- 294 | -- produces: 295 | -- 296 | -- { 297 | -- "city": "Kyoto", 298 | -- "climate": { 299 | -- "avg_temp": 16, 300 | -- "humidity": "high", 301 | -- "snowfall": "minimal" 302 | -- }, 303 | -- "country": "Japan", 304 | -- "wards": 11 305 | -- } 306 | -- 307 | -- which I must admit is kinda ugly, sorry. This was the default for 308 | -- encode_pretty() prior to version 20141223.14. 309 | -- 310 | -- 311 | -- HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA 312 | -- 313 | -- If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes, 314 | -- but as a sequence of UTF-8 characters. 315 | -- 316 | -- Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH 317 | -- separators, if found in a string, are encoded with a JSON escape instead of being dumped as is. 318 | -- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON 319 | -- to also be valid Java. 320 | -- 321 | -- AMBIGUOUS SITUATIONS DURING THE ENCODING 322 | -- 323 | -- During the encode, if a Lua table being encoded contains both string 324 | -- and numeric keys, it fits neither JSON's idea of an object, nor its 325 | -- idea of an array. To get around this, when any string key exists (or 326 | -- when non-positive numeric keys exist), numeric keys are converted to 327 | -- strings. 328 | -- 329 | -- For example, 330 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 331 | -- produces the JSON object 332 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 333 | -- 334 | -- To prohibit this conversion and instead make it an error condition, set 335 | -- JSON.noKeyConversion = true 336 | -- 337 | -- 338 | -- ENCODING JSON NULL VALUES 339 | -- 340 | -- Lua tables completely omit keys whose value is nil, so without special handling there's 341 | -- no way to represent JSON object's null value in a Lua table. For example 342 | -- JSON:encode({ username = "admin", password = nil }) 343 | -- 344 | -- produces: 345 | -- 346 | -- {"username":"admin"} 347 | -- 348 | -- In order to actually produce 349 | -- 350 | -- {"username":"admin", "password":null} 351 | -- 352 | 353 | -- one can include a string value for a "null" field in the options table passed to encode().... 354 | -- any Lua table entry with that value becomes null in the JSON output: 355 | -- 356 | -- JSON:encode({ username = "admin", password = "xyzzy" }, -- First arg is the Lua table to encode as JSON. 357 | -- nil, -- Second arg is the 'etc' value, ignored here 358 | -- { null = "xyzzy" }) -- Third arg is th options table 359 | -- 360 | -- produces: 361 | -- 362 | -- {"username":"admin", "password":null} 363 | -- 364 | -- Just be sure to use a string that is otherwise unlikely to appear in your data. 365 | -- The string "\0" (a string with one null byte) may well be appropriate for many applications. 366 | -- 367 | -- The "null" options also applies to Lua tables that become JSON arrays. 368 | -- JSON:encode({ "one", "two", nil, nil }) 369 | -- 370 | -- produces 371 | -- 372 | -- ["one","two"] 373 | -- 374 | -- while 375 | -- 376 | -- NullPlaceholder = "\0" 377 | -- encode_options = { null = NullPlaceholder } 378 | -- JSON:encode({ "one", "two", NullPlaceholder, NullPlaceholder}, nil, encode_options) 379 | -- produces 380 | -- 381 | -- ["one","two",null,null] 382 | -- 383 | -- 384 | -- 385 | -- HANDLING LARGE AND/OR PRECISE NUMBERS 386 | -- 387 | -- 388 | -- Without special handling, numbers in JSON can lose precision in Lua. 389 | -- For example: 390 | -- 391 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 392 | -- 393 | -- print("small: ", type(T.small), T.small) 394 | -- print("big: ", type(T.big), T.big) 395 | -- print("precise: ", type(T.precise), T.precise) 396 | -- 397 | -- produces 398 | -- 399 | -- small: number 12345 400 | -- big: number 1.2345678901235e+28 401 | -- precise: number 9876.6789012346 402 | -- 403 | -- Precision is lost with both 'big' and 'precise'. 404 | -- 405 | -- This package offers ways to try to handle this better (for some definitions of "better")... 406 | -- 407 | -- The most precise method is by setting the global: 408 | -- 409 | -- JSON.decodeNumbersAsObjects = true 410 | -- 411 | -- When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact 412 | -- JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string. 413 | -- 414 | -- This is done by encoding the numeric data with a Lua table/metatable that returns 415 | -- the possibly-imprecise numeric form when accessed numerically, but the original precise 416 | -- representation when accessed as a string. 417 | -- 418 | -- Consider the example above, with this option turned on: 419 | -- 420 | -- JSON.decodeNumbersAsObjects = true 421 | -- 422 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 423 | -- 424 | -- print("small: ", type(T.small), T.small) 425 | -- print("big: ", type(T.big), T.big) 426 | -- print("precise: ", type(T.precise), T.precise) 427 | -- 428 | -- This now produces: 429 | -- 430 | -- small: table 12345 431 | -- big: table 12345678901234567890123456789 432 | -- precise: table 9876.67890123456789012345 433 | -- 434 | -- However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric 435 | -- contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts 436 | -- and when the data finds its way to this package's encode() function, the original full-precision 437 | -- representation is used. 438 | -- 439 | -- You can force access to the string or numeric version via 440 | -- JSON:forceString() 441 | -- JSON:forceNumber() 442 | -- For example, 443 | -- local probably_okay = JSON:forceNumber(T.small) -- 'probably_okay' is a number 444 | -- 445 | -- Code the inspects the JSON-turned-Lua data using type() can run into troubles because what used to 446 | -- be a number can now be a table (e.g. as the small/big/precise example above shows). Update these 447 | -- situations to use JSON:isNumber(item), which returns nil if the item is neither a number nor one 448 | -- of these number objects. If it is either, it returns the number itself. For completeness there's 449 | -- also JSON:isString(item). 450 | -- 451 | -- If you want to try to avoid the hassles of this "number as an object" kludge for all but really 452 | -- big numbers, you can set JSON.decodeNumbersAsObjects and then also set one or both of 453 | -- JSON:decodeIntegerObjectificationLength 454 | -- JSON:decodeDecimalObjectificationLength 455 | -- They refer to the length of the part of the number before and after a decimal point. If they are 456 | -- set and their part is at least that number of digits, objectification occurs. If both are set, 457 | -- objectification occurs when either length is met. 458 | -- 459 | -- ----------------------- 460 | -- 461 | -- Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers in your Lua 462 | -- table that retain high precision upon encoding to JSON, by using the JSON:asNumber() function: 463 | -- 464 | -- T = { 465 | -- imprecise = 123456789123456789.123456789123456789, 466 | -- precise = JSON:asNumber("123456789123456789.123456789123456789") 467 | -- } 468 | -- 469 | -- print(JSON:encode_pretty(T)) 470 | -- 471 | -- This produces: 472 | -- 473 | -- { 474 | -- "precise": 123456789123456789.123456789123456789, 475 | -- "imprecise": 1.2345678912346e+17 476 | -- } 477 | -- 478 | -- 479 | -- ----------------------- 480 | -- 481 | -- A different way to handle big/precise JSON numbers is to have decode() merely return the exact 482 | -- string representation of the number instead of the number itself. This approach might be useful 483 | -- when the numbers are merely some kind of opaque object identifier and you want to work with them 484 | -- in Lua as strings anyway. 485 | -- 486 | -- This approach is enabled by setting 487 | -- 488 | -- JSON.decodeIntegerStringificationLength = 10 489 | -- 490 | -- The value is the number of digits (of the integer part of the number) at which to stringify numbers. 491 | -- NOTE: this setting is ignored if JSON.decodeNumbersAsObjects is true, as that takes precedence. 492 | -- 493 | -- Consider our previous example with this option set to 10: 494 | -- 495 | -- JSON.decodeIntegerStringificationLength = 10 496 | -- 497 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 498 | -- 499 | -- print("small: ", type(T.small), T.small) 500 | -- print("big: ", type(T.big), T.big) 501 | -- print("precise: ", type(T.precise), T.precise) 502 | -- 503 | -- This produces: 504 | -- 505 | -- small: number 12345 506 | -- big: string 12345678901234567890123456789 507 | -- precise: number 9876.6789012346 508 | -- 509 | -- The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits 510 | -- in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures 511 | -- that all JSON numeric data becomes strings in Lua. 512 | -- 513 | -- Note that unlike 514 | -- JSON.decodeNumbersAsObjects = true 515 | -- this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it. 516 | -- If the string is then converted back to JSON, it's still a string. After running the code above, adding 517 | -- print(JSON:encode(T)) 518 | -- produces 519 | -- {"big":"12345678901234567890123456789","precise":9876.6789012346,"small":12345} 520 | -- which is unlikely to be desired. 521 | -- 522 | -- There's a comparable option for the length of the decimal part of a number: 523 | -- 524 | -- JSON.decodeDecimalStringificationLength 525 | -- 526 | -- This can be used alone or in conjunction with 527 | -- 528 | -- JSON.decodeIntegerStringificationLength 529 | -- 530 | -- to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after 531 | -- the decimal point. (Both are ignored if JSON.decodeNumbersAsObjects is true.) 532 | -- 533 | -- This example: 534 | -- 535 | -- JSON.decodeIntegerStringificationLength = 10 536 | -- JSON.decodeDecimalStringificationLength = 5 537 | -- 538 | -- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') 539 | -- 540 | -- print("small: ", type(T.small), T.small) 541 | -- print("big: ", type(T.big), T.big) 542 | -- print("precise: ", type(T.precise), T.precise) 543 | -- 544 | -- produces: 545 | -- 546 | -- small: number 12345 547 | -- big: string 12345678901234567890123456789 548 | -- precise: string 9876.67890123456789012345 549 | -- 550 | -- 551 | -- HANDLING UNSUPPORTED VALUE TYPES 552 | -- 553 | -- Among the encoding errors that might be raised is an attempt to convert a table value that has a type 554 | -- that this package hasn't accounted for: a function, userdata, or a thread. You can handle these types as table 555 | -- values (but not as table keys) if you supply a JSON:unsupportedTypeEncoder() method along the lines of the 556 | -- following example: 557 | -- 558 | -- function JSON:unsupportedTypeEncoder(value_of_unsupported_type) 559 | -- if type(value_of_unsupported_type) == 'function' then 560 | -- return "a function value" 561 | -- else 562 | -- return nil 563 | -- end 564 | -- end 565 | -- 566 | -- Your unsupportedTypeEncoder() method is actually called with a bunch of arguments: 567 | -- 568 | -- self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key) 569 | -- 570 | -- The 'value' is the function, thread, or userdata to be converted to JSON. 571 | -- 572 | -- The 'etc' and 'options' arguments are those passed to the original encode(). The other arguments are 573 | -- probably of little interest; see the source code. (Note that 'for_key' is never true, as this function 574 | -- is invoked only on table values; table keys of these types still trigger the onEncodeError method.) 575 | -- 576 | -- If your unsupportedTypeEncoder() method returns a string, it's inserted into the JSON as is. 577 | -- If it returns nil plus an error message, that error message is passed through to an onEncodeError invocation. 578 | -- If it returns only nil, processing falls through to a default onEncodeError invocation. 579 | -- 580 | -- If you want to handle everything in a simple way: 581 | -- 582 | -- function JSON:unsupportedTypeEncoder(value) 583 | -- return tostring(value) 584 | -- end 585 | -- 586 | -- 587 | -- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT 588 | -- 589 | -- assert 590 | -- onDecodeError 591 | -- onDecodeOfNilError 592 | -- onDecodeOfHTMLError 593 | -- onTrailingGarbage 594 | -- onEncodeError 595 | -- unsupportedTypeEncoder 596 | -- 597 | -- If you want to create a separate Lua JSON object with its own error handlers, 598 | -- you can reload JSON.lua or use the :new() method. 599 | -- 600 | --------------------------------------------------------------------------- 601 | 602 | local default_pretty_indent = " " 603 | local default_pretty_options = { pretty = true, indent = default_pretty_indent, align_keys = false, array_newline = false } 604 | 605 | local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray 606 | local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject 607 | 608 | function OBJDEF:newArray(tbl) 609 | return setmetatable(tbl or {}, isArray) 610 | end 611 | 612 | function OBJDEF:newObject(tbl) 613 | return setmetatable(tbl or {}, isObject) 614 | end 615 | 616 | 617 | 618 | 619 | local function getnum(op) 620 | return type(op) == 'number' and op or op.N 621 | end 622 | 623 | local isNumber = { 624 | __tostring = function(T) return T.S end, 625 | __unm = function(op) return getnum(op) end, 626 | 627 | __concat = function(op1, op2) return tostring(op1) .. tostring(op2) end, 628 | __add = function(op1, op2) return getnum(op1) + getnum(op2) end, 629 | __sub = function(op1, op2) return getnum(op1) - getnum(op2) end, 630 | __mul = function(op1, op2) return getnum(op1) * getnum(op2) end, 631 | __div = function(op1, op2) return getnum(op1) / getnum(op2) end, 632 | __mod = function(op1, op2) return getnum(op1) % getnum(op2) end, 633 | __pow = function(op1, op2) return getnum(op1) ^ getnum(op2) end, 634 | __lt = function(op1, op2) return getnum(op1) < getnum(op2) end, 635 | __eq = function(op1, op2) return getnum(op1) == getnum(op2) end, 636 | __le = function(op1, op2) return getnum(op1) <= getnum(op2) end, 637 | } 638 | isNumber.__index = isNumber 639 | 640 | function OBJDEF:asNumber(item) 641 | 642 | if getmetatable(item) == isNumber then 643 | -- it's already a JSON number object. 644 | return item 645 | elseif type(item) == 'table' and type(item.S) == 'string' and type(item.N) == 'number' then 646 | -- it's a number-object table that lost its metatable, so give it one 647 | return setmetatable(item, isNumber) 648 | else 649 | -- the normal situation... given a number or a string representation of a number.... 650 | local holder = { 651 | S = tostring(item), -- S is the representation of the number as a string, which remains precise 652 | N = tonumber(item), -- N is the number as a Lua number. 653 | } 654 | return setmetatable(holder, isNumber) 655 | end 656 | end 657 | 658 | -- 659 | -- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, 660 | -- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert 661 | -- to a string in most cases, but it's here to allow it to be forced when needed. 662 | -- 663 | function OBJDEF:forceString(item) 664 | if type(item) == 'table' and type(item.S) == 'string' then 665 | return item.S 666 | else 667 | return tostring(item) 668 | end 669 | end 670 | 671 | -- 672 | -- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, 673 | -- return the numeric version. 674 | -- 675 | function OBJDEF:forceNumber(item) 676 | if type(item) == 'table' and type(item.N) == 'number' then 677 | return item.N 678 | else 679 | return tonumber(item) 680 | end 681 | end 682 | 683 | -- 684 | -- If the given item is a number, return it. Otherwise, return nil. 685 | -- This, this can be used both in a conditional and to access the number when you're not sure its form. 686 | -- 687 | function OBJDEF:isNumber(item) 688 | if type(item) == 'number' then 689 | return item 690 | elseif type(item) == 'table' and type(item.N) == 'number' then 691 | return item.N 692 | else 693 | return nil 694 | end 695 | end 696 | 697 | function OBJDEF:isString(item) 698 | if type(item) == 'string' then 699 | return item 700 | elseif type(item) == 'table' and type(item.S) == 'string' then 701 | return item.S 702 | else 703 | return nil 704 | end 705 | end 706 | 707 | 708 | local function unicode_codepoint_as_utf8(codepoint) 709 | -- 710 | -- codepoint is a number 711 | -- 712 | if codepoint <= 127 then 713 | return string.char(codepoint) 714 | 715 | elseif codepoint <= 2047 then 716 | -- 717 | -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 718 | -- 719 | local highpart = math.floor(codepoint / 0x40) 720 | local lowpart = codepoint - (0x40 * highpart) 721 | return string.char(0xC0 + highpart, 722 | 0x80 + lowpart) 723 | 724 | elseif codepoint <= 65535 then 725 | -- 726 | -- 1110yyyy 10yyyyxx 10xxxxxx 727 | -- 728 | local highpart = math.floor(codepoint / 0x1000) 729 | local remainder = codepoint - 0x1000 * highpart 730 | local midpart = math.floor(remainder / 0x40) 731 | local lowpart = remainder - 0x40 * midpart 732 | 733 | highpart = 0xE0 + highpart 734 | midpart = 0x80 + midpart 735 | lowpart = 0x80 + lowpart 736 | 737 | -- 738 | -- Check for an invalid character (thanks Andy R. at Adobe). 739 | -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 740 | -- 741 | if ( highpart == 0xE0 and midpart < 0xA0 ) or 742 | ( highpart == 0xED and midpart > 0x9F ) or 743 | ( highpart == 0xF0 and midpart < 0x90 ) or 744 | ( highpart == 0xF4 and midpart > 0x8F ) 745 | then 746 | return "?" 747 | else 748 | return string.char(highpart, 749 | midpart, 750 | lowpart) 751 | end 752 | 753 | else 754 | -- 755 | -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx 756 | -- 757 | local highpart = math.floor(codepoint / 0x40000) 758 | local remainder = codepoint - 0x40000 * highpart 759 | local midA = math.floor(remainder / 0x1000) 760 | remainder = remainder - 0x1000 * midA 761 | local midB = math.floor(remainder / 0x40) 762 | local lowpart = remainder - 0x40 * midB 763 | 764 | return string.char(0xF0 + highpart, 765 | 0x80 + midA, 766 | 0x80 + midB, 767 | 0x80 + lowpart) 768 | end 769 | end 770 | 771 | function OBJDEF:onDecodeError(message, text, location, etc) 772 | if text then 773 | if location then 774 | message = string.format("%s at byte %d of: %s", message, location, text) 775 | else 776 | message = string.format("%s: %s", message, text) 777 | end 778 | end 779 | 780 | if etc ~= nil then 781 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 782 | end 783 | 784 | if self.assert then 785 | self.assert(false, message) 786 | else 787 | assert(false, message) 788 | end 789 | end 790 | 791 | function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc) 792 | return self:onDecodeError("trailing garbage", json_text, location, etc) 793 | end 794 | 795 | OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError 796 | OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError 797 | 798 | function OBJDEF:onEncodeError(message, etc) 799 | if etc ~= nil then 800 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 801 | end 802 | 803 | if self.assert then 804 | self.assert(false, message) 805 | else 806 | assert(false, message) 807 | end 808 | end 809 | 810 | local function grok_number(self, text, start, options) 811 | -- 812 | -- Grab the integer part 813 | -- 814 | local integer_part = text:match('^-?[1-9]%d*', start) 815 | or text:match("^-?0", start) 816 | 817 | if not integer_part then 818 | self:onDecodeError("expected number", text, start, options.etc) 819 | return nil, start -- in case the error method doesn't abort, return something sensible 820 | end 821 | 822 | local i = start + integer_part:len() 823 | 824 | -- 825 | -- Grab an optional decimal part 826 | -- 827 | local decimal_part = text:match('^%.%d+', i) or "" 828 | 829 | i = i + decimal_part:len() 830 | 831 | -- 832 | -- Grab an optional exponential part 833 | -- 834 | local exponent_part = text:match('^[eE][-+]?%d+', i) or "" 835 | 836 | i = i + exponent_part:len() 837 | 838 | local full_number_text = integer_part .. decimal_part .. exponent_part 839 | 840 | if options.decodeNumbersAsObjects then 841 | 842 | local objectify = false 843 | 844 | if not options.decodeIntegerObjectificationLength and not options.decodeDecimalObjectificationLength then 845 | -- no options, so objectify 846 | objectify = true 847 | 848 | elseif (options.decodeIntegerObjectificationLength 849 | and 850 | (integer_part:len() >= options.decodeIntegerObjectificationLength or exponent_part:len() > 0)) 851 | 852 | or 853 | (options.decodeDecimalObjectificationLength 854 | and 855 | (decimal_part:len() >= options.decodeDecimalObjectificationLength or exponent_part:len() > 0)) 856 | then 857 | -- have options and they are triggered, so objectify 858 | objectify = true 859 | end 860 | 861 | if objectify then 862 | return OBJDEF:asNumber(full_number_text), i 863 | end 864 | -- else, fall through to try to return as a straight-up number 865 | 866 | else 867 | 868 | -- Not always decoding numbers as objects, so perhaps encode as strings? 869 | 870 | -- 871 | -- If we're told to stringify only under certain conditions, so do. 872 | -- We punt a bit when there's an exponent by just stringifying no matter what. 873 | -- I suppose we should really look to see whether the exponent is actually big enough one 874 | -- way or the other to trip stringification, but I'll be lazy about it until someone asks. 875 | -- 876 | if (options.decodeIntegerStringificationLength 877 | and 878 | (integer_part:len() >= options.decodeIntegerStringificationLength or exponent_part:len() > 0)) 879 | 880 | or 881 | 882 | (options.decodeDecimalStringificationLength 883 | and 884 | (decimal_part:len() >= options.decodeDecimalStringificationLength or exponent_part:len() > 0)) 885 | then 886 | return full_number_text, i -- this returns the exact string representation seen in the original JSON 887 | end 888 | 889 | end 890 | 891 | 892 | local as_number = tonumber(full_number_text) 893 | 894 | if not as_number then 895 | self:onDecodeError("bad number", text, start, options.etc) 896 | return nil, start -- in case the error method doesn't abort, return something sensible 897 | end 898 | 899 | return as_number, i 900 | end 901 | 902 | 903 | local function grok_string(self, text, start, options) 904 | 905 | if text:sub(start,start) ~= '"' then 906 | self:onDecodeError("expected string's opening quote", text, start, options.etc) 907 | return nil, start -- in case the error method doesn't abort, return something sensible 908 | end 909 | 910 | local i = start + 1 -- +1 to bypass the initial quote 911 | local text_len = text:len() 912 | local VALUE = "" 913 | while i <= text_len do 914 | local c = text:sub(i,i) 915 | if c == '"' then 916 | return VALUE, i + 1 917 | end 918 | if c ~= '\\' then 919 | VALUE = VALUE .. c 920 | i = i + 1 921 | elseif text:match('^\\b', i) then 922 | VALUE = VALUE .. "\b" 923 | i = i + 2 924 | elseif text:match('^\\f', i) then 925 | VALUE = VALUE .. "\f" 926 | i = i + 2 927 | elseif text:match('^\\n', i) then 928 | VALUE = VALUE .. "\n" 929 | i = i + 2 930 | elseif text:match('^\\r', i) then 931 | VALUE = VALUE .. "\r" 932 | i = i + 2 933 | elseif text:match('^\\t', i) then 934 | VALUE = VALUE .. "\t" 935 | i = i + 2 936 | else 937 | local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 938 | if hex then 939 | i = i + 6 -- bypass what we just read 940 | 941 | -- We have a Unicode codepoint. It could be standalone, or if in the proper range and 942 | -- followed by another in a specific range, it'll be a two-code surrogate pair. 943 | local codepoint = tonumber(hex, 16) 944 | if codepoint >= 0xD800 and codepoint <= 0xDBFF then 945 | -- it's a hi surrogate... see whether we have a following low 946 | local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 947 | if lo_surrogate then 948 | i = i + 6 -- bypass the low surrogate we just read 949 | codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) 950 | else 951 | -- not a proper low, so we'll just leave the first codepoint as is and spit it out. 952 | end 953 | end 954 | VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) 955 | 956 | else 957 | 958 | -- just pass through what's escaped 959 | VALUE = VALUE .. text:match('^\\(.)', i) 960 | i = i + 2 961 | end 962 | end 963 | end 964 | 965 | self:onDecodeError("unclosed string", text, start, options.etc) 966 | return nil, start -- in case the error method doesn't abort, return something sensible 967 | end 968 | 969 | local function skip_whitespace(text, start) 970 | 971 | local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 972 | if match_end then 973 | return match_end + 1 974 | else 975 | return start 976 | end 977 | end 978 | 979 | local grok_one -- assigned later 980 | 981 | local function grok_object(self, text, start, options) 982 | 983 | if text:sub(start,start) ~= '{' then 984 | self:onDecodeError("expected '{'", text, start, options.etc) 985 | return nil, start -- in case the error method doesn't abort, return something sensible 986 | end 987 | 988 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' 989 | 990 | local VALUE = self.strictTypes and self:newObject { } or { } 991 | 992 | if text:sub(i,i) == '}' then 993 | return VALUE, i + 1 994 | end 995 | local text_len = text:len() 996 | while i <= text_len do 997 | local key, new_i = grok_string(self, text, i, options) 998 | 999 | i = skip_whitespace(text, new_i) 1000 | 1001 | if text:sub(i, i) ~= ':' then 1002 | self:onDecodeError("expected colon", text, i, options.etc) 1003 | return nil, i -- in case the error method doesn't abort, return something sensible 1004 | end 1005 | 1006 | i = skip_whitespace(text, i + 1) 1007 | 1008 | local new_val, new_i = grok_one(self, text, i, options) 1009 | 1010 | VALUE[key] = new_val 1011 | 1012 | -- 1013 | -- Expect now either '}' to end things, or a ',' to allow us to continue. 1014 | -- 1015 | i = skip_whitespace(text, new_i) 1016 | 1017 | local c = text:sub(i,i) 1018 | 1019 | if c == '}' then 1020 | return VALUE, i + 1 1021 | end 1022 | 1023 | if text:sub(i, i) ~= ',' then 1024 | self:onDecodeError("expected comma or '}'", text, i, options.etc) 1025 | return nil, i -- in case the error method doesn't abort, return something sensible 1026 | end 1027 | 1028 | i = skip_whitespace(text, i + 1) 1029 | end 1030 | 1031 | self:onDecodeError("unclosed '{'", text, start, options.etc) 1032 | return nil, start -- in case the error method doesn't abort, return something sensible 1033 | end 1034 | 1035 | local function grok_array(self, text, start, options) 1036 | if text:sub(start,start) ~= '[' then 1037 | self:onDecodeError("expected '['", text, start, options.etc) 1038 | return nil, start -- in case the error method doesn't abort, return something sensible 1039 | end 1040 | 1041 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' 1042 | local VALUE = self.strictTypes and self:newArray { } or { } 1043 | if text:sub(i,i) == ']' then 1044 | return VALUE, i + 1 1045 | end 1046 | 1047 | local VALUE_INDEX = 1 1048 | 1049 | local text_len = text:len() 1050 | while i <= text_len do 1051 | local val, new_i = grok_one(self, text, i, options) 1052 | 1053 | -- can't table.insert(VALUE, val) here because it's a no-op if val is nil 1054 | VALUE[VALUE_INDEX] = val 1055 | VALUE_INDEX = VALUE_INDEX + 1 1056 | 1057 | i = skip_whitespace(text, new_i) 1058 | 1059 | -- 1060 | -- Expect now either ']' to end things, or a ',' to allow us to continue. 1061 | -- 1062 | local c = text:sub(i,i) 1063 | if c == ']' then 1064 | return VALUE, i + 1 1065 | end 1066 | if text:sub(i, i) ~= ',' then 1067 | self:onDecodeError("expected comma or ']'", text, i, options.etc) 1068 | return nil, i -- in case the error method doesn't abort, return something sensible 1069 | end 1070 | i = skip_whitespace(text, i + 1) 1071 | end 1072 | self:onDecodeError("unclosed '['", text, start, options.etc) 1073 | return nil, i -- in case the error method doesn't abort, return something sensible 1074 | end 1075 | 1076 | 1077 | grok_one = function(self, text, start, options) 1078 | -- Skip any whitespace 1079 | start = skip_whitespace(text, start) 1080 | 1081 | if start > text:len() then 1082 | self:onDecodeError("unexpected end of string", text, nil, options.etc) 1083 | return nil, start -- in case the error method doesn't abort, return something sensible 1084 | end 1085 | 1086 | if text:find('^"', start) then 1087 | return grok_string(self, text, start, options.etc) 1088 | 1089 | elseif text:find('^[-0123456789 ]', start) then 1090 | return grok_number(self, text, start, options) 1091 | 1092 | elseif text:find('^%{', start) then 1093 | return grok_object(self, text, start, options) 1094 | 1095 | elseif text:find('^%[', start) then 1096 | return grok_array(self, text, start, options) 1097 | 1098 | elseif text:find('^true', start) then 1099 | return true, start + 4 1100 | 1101 | elseif text:find('^false', start) then 1102 | return false, start + 5 1103 | 1104 | elseif text:find('^null', start) then 1105 | return options.null, start + 4 1106 | 1107 | else 1108 | self:onDecodeError("can't parse JSON", text, start, options.etc) 1109 | return nil, 1 -- in case the error method doesn't abort, return something sensible 1110 | end 1111 | end 1112 | 1113 | function OBJDEF:decode(text, etc, options) 1114 | -- 1115 | -- If the user didn't pass in a table of decode options, make an empty one. 1116 | -- 1117 | if type(options) ~= 'table' then 1118 | options = {} 1119 | end 1120 | 1121 | -- 1122 | -- If they passed in an 'etc' argument, stuff it into the options. 1123 | -- (If not, any 'etc' field in the options they passed in remains to be used) 1124 | -- 1125 | if etc ~= nil then 1126 | options.etc = etc 1127 | end 1128 | 1129 | 1130 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1131 | local error_message = "JSON:decode must be called in method format" 1132 | OBJDEF:onDecodeError(error_message, nil, nil, options.etc) 1133 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1134 | end 1135 | 1136 | if text == nil then 1137 | local error_message = "nil passed to JSON:decode()" 1138 | self:onDecodeOfNilError(error_message, nil, nil, options.etc) 1139 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1140 | 1141 | elseif type(text) ~= 'string' then 1142 | local error_message = "expected string argument to JSON:decode()" 1143 | self:onDecodeError(string.format("%s, got %s", error_message, type(text)), nil, nil, options.etc) 1144 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1145 | end 1146 | 1147 | if text:match('^%s*$') then 1148 | -- an empty string is nothing, but not an error 1149 | return nil 1150 | end 1151 | 1152 | if text:match('^%s*<') then 1153 | -- Can't be JSON... we'll assume it's HTML 1154 | local error_message = "HTML passed to JSON:decode()" 1155 | self:onDecodeOfHTMLError(error_message, text, nil, options.etc) 1156 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1157 | end 1158 | 1159 | -- 1160 | -- Ensure that it's not UTF-32 or UTF-16. 1161 | -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), 1162 | -- but this package can't handle them. 1163 | -- 1164 | if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then 1165 | local error_message = "JSON package groks only UTF-8, sorry" 1166 | self:onDecodeError(error_message, text, nil, options.etc) 1167 | return nil, error_message -- in case the error method doesn't abort, return something sensible 1168 | end 1169 | 1170 | -- 1171 | -- apply global options 1172 | -- 1173 | if options.decodeNumbersAsObjects == nil then 1174 | options.decodeNumbersAsObjects = self.decodeNumbersAsObjects 1175 | end 1176 | if options.decodeIntegerObjectificationLength == nil then 1177 | options.decodeIntegerObjectificationLength = self.decodeIntegerObjectificationLength 1178 | end 1179 | if options.decodeDecimalObjectificationLength == nil then 1180 | options.decodeDecimalObjectificationLength = self.decodeDecimalObjectificationLength 1181 | end 1182 | if options.decodeIntegerStringificationLength == nil then 1183 | options.decodeIntegerStringificationLength = self.decodeIntegerStringificationLength 1184 | end 1185 | if options.decodeDecimalStringificationLength == nil then 1186 | options.decodeDecimalStringificationLength = self.decodeDecimalStringificationLength 1187 | end 1188 | 1189 | 1190 | -- 1191 | -- Finally, go parse it 1192 | -- 1193 | local success, value, next_i = pcall(grok_one, self, text, 1, options) 1194 | 1195 | if success then 1196 | 1197 | local error_message = nil 1198 | if next_i ~= #text + 1 then 1199 | -- something's left over after we parsed the first thing.... whitespace is allowed. 1200 | next_i = skip_whitespace(text, next_i) 1201 | 1202 | -- if we have something left over now, it's trailing garbage 1203 | if next_i ~= #text + 1 then 1204 | value, error_message = self:onTrailingGarbage(text, next_i, value, options.etc) 1205 | end 1206 | end 1207 | return value, error_message 1208 | 1209 | else 1210 | 1211 | -- If JSON:onDecodeError() didn't abort out of the pcall, we'll have received 1212 | -- the error message here as "value", so pass it along as an assert. 1213 | local error_message = value 1214 | if self.assert then 1215 | self.assert(false, error_message) 1216 | else 1217 | assert(false, error_message) 1218 | end 1219 | -- ...and if we're still here (because the assert didn't throw an error), 1220 | -- return a nil and throw the error message on as a second arg 1221 | return nil, error_message 1222 | 1223 | end 1224 | end 1225 | 1226 | local function backslash_replacement_function(c) 1227 | if c == "\n" then 1228 | return "\\n" 1229 | elseif c == "\r" then 1230 | return "\\r" 1231 | elseif c == "\t" then 1232 | return "\\t" 1233 | elseif c == "\b" then 1234 | return "\\b" 1235 | elseif c == "\f" then 1236 | return "\\f" 1237 | elseif c == '"' then 1238 | return '\\"' 1239 | elseif c == '\\' then 1240 | return '\\\\' 1241 | else 1242 | return string.format("\\u%04x", c:byte()) 1243 | end 1244 | end 1245 | 1246 | local chars_to_be_escaped_in_JSON_string 1247 | = '[' 1248 | .. '"' -- class sub-pattern to match a double quote 1249 | .. '%\\' -- class sub-pattern to match a backslash 1250 | .. '%z' -- class sub-pattern to match a null 1251 | .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters 1252 | .. ']' 1253 | 1254 | 1255 | local LINE_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2028) 1256 | local PARAGRAPH_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2029) 1257 | local function json_string_literal(value, options) 1258 | local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) 1259 | if options.stringsAreUtf8 then 1260 | -- 1261 | -- This feels really ugly to just look into a string for the sequence of bytes that we know to be a particular utf8 character, 1262 | -- but utf8 was designed purposefully to make this kind of thing possible. Still, feels dirty. 1263 | -- I'd rather decode the byte stream into a character stream, but it's not technically needed so 1264 | -- not technically worth it. 1265 | -- 1266 | newval = newval:gsub(LINE_SEPARATOR_as_utf8, '\\u2028'):gsub(PARAGRAPH_SEPARATOR_as_utf8,'\\u2029') 1267 | end 1268 | return '"' .. newval .. '"' 1269 | end 1270 | 1271 | local function object_or_array(self, T, etc) 1272 | -- 1273 | -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON 1274 | -- object. If there are only numbers, it's a JSON array. 1275 | -- 1276 | -- If we'll be converting to a JSON object, we'll want to sort the keys so that the 1277 | -- end result is deterministic. 1278 | -- 1279 | local string_keys = { } 1280 | local number_keys = { } 1281 | local number_keys_must_be_strings = false 1282 | local maximum_number_key 1283 | 1284 | for key in pairs(T) do 1285 | if type(key) == 'string' then 1286 | table.insert(string_keys, key) 1287 | elseif type(key) == 'number' then 1288 | table.insert(number_keys, key) 1289 | if key <= 0 or key >= math.huge then 1290 | number_keys_must_be_strings = true 1291 | elseif not maximum_number_key or key > maximum_number_key then 1292 | maximum_number_key = key 1293 | end 1294 | elseif type(key) == 'boolean' then 1295 | table.insert(string_keys, tostring(key)) 1296 | else 1297 | self:onEncodeError("can't encode table with a key of type " .. type(key), etc) 1298 | end 1299 | end 1300 | 1301 | if #string_keys == 0 and not number_keys_must_be_strings then 1302 | -- 1303 | -- An empty table, or a numeric-only array 1304 | -- 1305 | if #number_keys > 0 then 1306 | return nil, maximum_number_key -- an array 1307 | elseif tostring(T) == "JSON array" then 1308 | return nil 1309 | elseif tostring(T) == "JSON object" then 1310 | return { } 1311 | else 1312 | -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects 1313 | return nil 1314 | end 1315 | end 1316 | 1317 | table.sort(string_keys) 1318 | 1319 | local map 1320 | if #number_keys > 0 then 1321 | -- 1322 | -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array 1323 | -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. 1324 | -- 1325 | 1326 | if self.noKeyConversion then 1327 | self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) 1328 | end 1329 | 1330 | -- 1331 | -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings 1332 | -- 1333 | map = { } 1334 | for key, val in pairs(T) do 1335 | map[key] = val 1336 | end 1337 | 1338 | table.sort(number_keys) 1339 | 1340 | -- 1341 | -- Throw numeric keys in there as strings 1342 | -- 1343 | for _, number_key in ipairs(number_keys) do 1344 | local string_key = tostring(number_key) 1345 | if map[string_key] == nil then 1346 | table.insert(string_keys , string_key) 1347 | map[string_key] = T[number_key] 1348 | else 1349 | self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) 1350 | end 1351 | end 1352 | end 1353 | 1354 | return string_keys, nil, map 1355 | end 1356 | 1357 | -- 1358 | -- Encode 1359 | -- 1360 | -- 'options' is nil, or a table with possible keys: 1361 | -- 1362 | -- pretty -- If true, return a pretty-printed version. 1363 | -- 1364 | -- indent -- A string (usually of spaces) used to indent each nested level. 1365 | -- 1366 | -- align_keys -- If true, align all the keys when formatting a table. The result is uglier than one might at first imagine. 1367 | -- Results are undefined if 'align_keys' is true but 'pretty' is not. 1368 | -- 1369 | -- array_newline -- If true, array elements are formatted each to their own line. The default is to all fall inline. 1370 | -- Results are undefined if 'array_newline' is true but 'pretty' is not. 1371 | -- 1372 | -- null -- If this exists with a string value, table elements with this value are output as JSON null. 1373 | -- 1374 | -- stringsAreUtf8 -- If true, consider Lua strings not as a sequence of bytes, but as a sequence of UTF-8 characters. 1375 | -- (Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH 1376 | -- separators, if found in a string, are encoded with a JSON escape instead of as raw UTF-8. 1377 | -- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON 1378 | -- to also be valid Java.) 1379 | -- 1380 | -- 1381 | local function encode_value(self, value, parents, etc, options, indent, for_key) 1382 | 1383 | -- 1384 | -- keys in a JSON object can never be null, so we don't even consider options.null when converting a key value 1385 | -- 1386 | if value == nil or (not for_key and options and options.null and value == options.null) then 1387 | return 'null' 1388 | 1389 | elseif type(value) == 'string' then 1390 | return json_string_literal(value, options) 1391 | 1392 | elseif type(value) == 'number' then 1393 | if value ~= value then 1394 | -- 1395 | -- NaN (Not a Number). 1396 | -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. 1397 | -- 1398 | return "null" 1399 | elseif value >= math.huge then 1400 | -- 1401 | -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should 1402 | -- really be a package option. Note: at least with some implementations, positive infinity 1403 | -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. 1404 | -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" 1405 | -- case first. 1406 | -- 1407 | return "1e+9999" 1408 | elseif value <= -math.huge then 1409 | -- 1410 | -- Negative infinity. 1411 | -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. 1412 | -- 1413 | return "-1e+9999" 1414 | else 1415 | return tostring(value) 1416 | end 1417 | 1418 | elseif type(value) == 'boolean' then 1419 | return tostring(value) 1420 | 1421 | elseif type(value) ~= 'table' then 1422 | 1423 | if self.unsupportedTypeEncoder then 1424 | local user_value, user_error = self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key) 1425 | -- If the user's handler returns a string, use that. If it returns nil plus an error message, bail with that. 1426 | -- If only nil returned, fall through to the default error handler. 1427 | if type(user_value) == 'string' then 1428 | return user_value 1429 | elseif user_value ~= nil then 1430 | self:onEncodeError("unsupportedTypeEncoder method returned a " .. type(user_value), etc) 1431 | elseif user_error then 1432 | self:onEncodeError(tostring(user_error), etc) 1433 | end 1434 | end 1435 | 1436 | self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) 1437 | 1438 | elseif getmetatable(value) == isNumber then 1439 | return tostring(value) 1440 | else 1441 | -- 1442 | -- A table to be converted to either a JSON object or array. 1443 | -- 1444 | local T = value 1445 | 1446 | if type(options) ~= 'table' then 1447 | options = {} 1448 | end 1449 | if type(indent) ~= 'string' then 1450 | indent = "" 1451 | end 1452 | 1453 | if parents[T] then 1454 | self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) 1455 | else 1456 | parents[T] = true 1457 | end 1458 | 1459 | local result_value 1460 | 1461 | local object_keys, maximum_number_key, map = object_or_array(self, T, etc) 1462 | if maximum_number_key then 1463 | -- 1464 | -- An array... 1465 | -- 1466 | local key_indent 1467 | if options.array_newline then 1468 | key_indent = indent .. tostring(options.indent or "") 1469 | else 1470 | key_indent = indent 1471 | end 1472 | 1473 | local ITEMS = { } 1474 | for i = 1, maximum_number_key do 1475 | table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, key_indent)) 1476 | end 1477 | 1478 | if options.array_newline then 1479 | result_value = "[\n" .. key_indent .. table.concat(ITEMS, ",\n" .. key_indent) .. "\n" .. indent .. "]" 1480 | elseif options.pretty then 1481 | result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" 1482 | else 1483 | result_value = "[" .. table.concat(ITEMS, ",") .. "]" 1484 | end 1485 | 1486 | elseif object_keys then 1487 | -- 1488 | -- An object 1489 | -- 1490 | local TT = map or T 1491 | 1492 | if options.pretty then 1493 | 1494 | local KEYS = { } 1495 | local max_key_length = 0 1496 | for _, key in ipairs(object_keys) do 1497 | local encoded = encode_value(self, tostring(key), parents, etc, options, indent, true) 1498 | if options.align_keys then 1499 | max_key_length = math.max(max_key_length, #encoded) 1500 | end 1501 | table.insert(KEYS, encoded) 1502 | end 1503 | local key_indent = indent .. tostring(options.indent or "") 1504 | local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") 1505 | local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" 1506 | 1507 | local COMBINED_PARTS = { } 1508 | for i, key in ipairs(object_keys) do 1509 | local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) 1510 | table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) 1511 | end 1512 | result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" 1513 | 1514 | else 1515 | 1516 | local PARTS = { } 1517 | for _, key in ipairs(object_keys) do 1518 | local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) 1519 | local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent, true) 1520 | table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) 1521 | end 1522 | result_value = "{" .. table.concat(PARTS, ",") .. "}" 1523 | 1524 | end 1525 | else 1526 | -- 1527 | -- An empty array/object... we'll treat it as an array, though it should really be an option 1528 | -- 1529 | result_value = "[]" 1530 | end 1531 | 1532 | parents[T] = false 1533 | return result_value 1534 | end 1535 | end 1536 | 1537 | local function top_level_encode(self, value, etc, options) 1538 | local val = encode_value(self, value, {}, etc, options) 1539 | if val == nil then 1540 | --PRIVATE("may need to revert to the previous public verison if I can't figure out what the guy wanted") 1541 | return val 1542 | else 1543 | return val 1544 | end 1545 | end 1546 | 1547 | function OBJDEF:encode(value, etc, options) 1548 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1549 | OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) 1550 | end 1551 | 1552 | -- 1553 | -- If the user didn't pass in a table of decode options, make an empty one. 1554 | -- 1555 | if type(options) ~= 'table' then 1556 | options = {} 1557 | end 1558 | 1559 | return top_level_encode(self, value, etc, options) 1560 | end 1561 | 1562 | function OBJDEF:encode_pretty(value, etc, options) 1563 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 1564 | OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) 1565 | end 1566 | 1567 | -- 1568 | -- If the user didn't pass in a table of decode options, use the default pretty ones 1569 | -- 1570 | if type(options) ~= 'table' then 1571 | options = default_pretty_options 1572 | end 1573 | 1574 | return top_level_encode(self, value, etc, options) 1575 | end 1576 | 1577 | function OBJDEF.__tostring() 1578 | return "JSON encode/decode package" 1579 | end 1580 | 1581 | OBJDEF.__index = OBJDEF 1582 | 1583 | function OBJDEF:new(args) 1584 | local new = { } 1585 | 1586 | if args then 1587 | for key, val in pairs(args) do 1588 | new[key] = val 1589 | end 1590 | end 1591 | 1592 | return setmetatable(new, OBJDEF) 1593 | end 1594 | 1595 | JSON = OBJDEF:new() -- Noita expects to just dofile everything :( 1596 | --return OBJDEF:new() 1597 | 1598 | -- 1599 | -- Version history: 1600 | -- 1601 | -- 20170927.26 Use option.null in decoding as well. Thanks to Max Sindwani for the bump, and sorry to Oliver Hitz 1602 | -- whose first mention of it four years ago was completely missed by me. 1603 | -- 1604 | -- 20170823.25 Added support for JSON:unsupportedTypeEncoder(). 1605 | -- Thanks to Chronos Phaenon Eosphoros (https://github.com/cpeosphoros) for the idea. 1606 | -- 1607 | -- 20170819.24 Added support for boolean keys in tables. 1608 | -- 1609 | -- 20170416.23 Added the "array_newline" formatting option suggested by yurenchen (http://www.yurenchen.com/) 1610 | -- 1611 | -- 20161128.22 Added: 1612 | -- JSON:isString() 1613 | -- JSON:isNumber() 1614 | -- JSON:decodeIntegerObjectificationLength 1615 | -- JSON:decodeDecimalObjectificationLength 1616 | -- 1617 | -- 20161109.21 Oops, had a small boo-boo in the previous update. 1618 | -- 1619 | -- 20161103.20 Used to silently ignore trailing garbage when decoding. Now fails via JSON:onTrailingGarbage() 1620 | -- http://seriot.ch/parsing_json.php 1621 | -- 1622 | -- Built-in error message about "expected comma or ']'" had mistakenly referred to '[' 1623 | -- 1624 | -- Updated the built-in error reporting to refer to bytes rather than characters. 1625 | -- 1626 | -- The decode() method no longer assumes that error handlers abort. 1627 | -- 1628 | -- Made the VERSION string a string instead of a number 1629 | -- 1630 | 1631 | -- 20160916.19 Fixed the isNumber.__index assignment (thanks to Jack Taylor) 1632 | -- 1633 | -- 20160730.18 Added JSON:forceString() and JSON:forceNumber() 1634 | -- 1635 | -- 20160728.17 Added concatenation to the metatable for JSON:asNumber() 1636 | -- 1637 | -- 20160709.16 Could crash if not passed an options table (thanks jarno heikkinen ). 1638 | -- 1639 | -- Made JSON:asNumber() a bit more resilient to being passed the results of itself. 1640 | -- 1641 | -- 20160526.15 Added the ability to easily encode null values in JSON, via the new "null" encoding option. 1642 | -- (Thanks to Adam B for bringing up the issue.) 1643 | -- 1644 | -- Added some support for very large numbers and precise floats via 1645 | -- JSON.decodeNumbersAsObjects 1646 | -- JSON.decodeIntegerStringificationLength 1647 | -- JSON.decodeDecimalStringificationLength 1648 | -- 1649 | -- Added the "stringsAreUtf8" encoding option. (Hat tip to http://lua-users.org/wiki/JsonModules ) 1650 | -- 1651 | -- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really 1652 | -- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines 1653 | -- more flexible, and changed the default encode_pretty() to be more generally useful. 1654 | -- 1655 | -- Added a third 'options' argument to the encode() and encode_pretty() routines, to control 1656 | -- how the encoding takes place. 1657 | -- 1658 | -- Updated docs to add assert() call to the loadfile() line, just as good practice so that 1659 | -- if there is a problem loading JSON.lua, the appropriate error message will percolate up. 1660 | -- 1661 | -- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, 1662 | -- so that the source of the package, and its version number, are visible in compiled copies. 1663 | -- 1664 | -- 20140911.12 Minor lua cleanup. 1665 | -- Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. 1666 | -- (Thanks to SmugMug's David Parry for these.) 1667 | -- 1668 | -- 20140418.11 JSON nulls embedded within an array were being ignored, such that 1669 | -- ["1",null,null,null,null,null,"seven"], 1670 | -- would return 1671 | -- {1,"seven"} 1672 | -- It's now fixed to properly return 1673 | -- {1, nil, nil, nil, nil, nil, "seven"} 1674 | -- Thanks to "haddock" for catching the error. 1675 | -- 1676 | -- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. 1677 | -- 1678 | -- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", 1679 | -- and this caused some problems. 1680 | -- 1681 | -- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate, 1682 | -- and had of course diverged (encode_pretty didn't get the fixes that encode got, so 1683 | -- sometimes produced incorrect results; thanks to Mattie for the heads up). 1684 | -- 1685 | -- Handle encoding tables with non-positive numeric keys (unlikely, but possible). 1686 | -- 1687 | -- If a table has both numeric and string keys, or its numeric keys are inappropriate 1688 | -- (such as being non-positive or infinite), the numeric keys are turned into 1689 | -- string keys appropriate for a JSON object. So, as before, 1690 | -- JSON:encode({ "one", "two", "three" }) 1691 | -- produces the array 1692 | -- ["one","two","three"] 1693 | -- but now something with mixed key types like 1694 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 1695 | -- instead of throwing an error produces an object: 1696 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 1697 | -- 1698 | -- To maintain the prior throw-an-error semantics, set 1699 | -- JSON.noKeyConversion = true 1700 | -- 1701 | -- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. 1702 | -- 1703 | -- 20130120.6 Comment update: added a link to the specific page on my blog where this code can 1704 | -- be found, so that folks who come across the code outside of my blog can find updates 1705 | -- more easily. 1706 | -- 1707 | -- 20111207.5 Added support for the 'etc' arguments, for better error reporting. 1708 | -- 1709 | -- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. 1710 | -- 1711 | -- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: 1712 | -- 1713 | -- * When encoding lua for JSON, Sparse numeric arrays are now handled by 1714 | -- spitting out full arrays, such that 1715 | -- JSON:encode({"one", "two", [10] = "ten"}) 1716 | -- returns 1717 | -- ["one","two",null,null,null,null,null,null,null,"ten"] 1718 | -- 1719 | -- In 20100810.2 and earlier, only up to the first non-null value would have been retained. 1720 | -- 1721 | -- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". 1722 | -- Version 20100810.2 and earlier created invalid JSON in both cases. 1723 | -- 1724 | -- * Unicode surrogate pairs are now detected when decoding JSON. 1725 | -- 1726 | -- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding 1727 | -- 1728 | -- 20100731.1 initial public release 1729 | -- 1730 | --------------------------------------------------------------------------------