ue-bridge 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rabbit
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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.5
2
+ Name: ue-bridge
3
+ Version: 1.0.0
4
+ Summary: Talk to a running UE4SS game: inspect objects, call functions, run Lua, while it plays. An MCP server and CLI plus the UEBridge mod.
5
+ Project-URL: Source, https://github.com/littleRabbit94/ue-bridge
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: lua,mcp,modding,ue4ss,unreal
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Games/Entertainment
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: mcp<2,>=1.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # ue-bridge
18
+
19
+ Talk to a running [UE4SS](https://github.com/UE4SS-RE/RE-UE4SS) game: inspect objects, call
20
+ functions, run Lua, while it plays. Two parts, released separately:
21
+
22
+ | Part | What | Where it goes |
23
+ |---|---|---|
24
+ | **UEBridge** (Lua mod) | Polls `ue4ss\bridge\request.json`, runs the request on the game thread, writes `response.json`. Game-agnostic. | The game: `ue4ss\Mods\UEBridge\` (Nexus / GitHub release zip) |
25
+ | **ue-bridge** (Python) | An MCP server and CLI that write those files and read the answers. Finds the running game by itself. | Your machine: `uvx ue-bridge` or `pip install ue-bridge` |
26
+
27
+ ```
28
+ AI agent / script -> ue-bridge (MCP stdio or HTTP, or CLI) -> request.json
29
+ UEBridge mod: ExecuteInGameThread
30
+ ue-bridge <- response.json <-
31
+ ```
32
+
33
+ No sockets in the game, no admin rights, no changes to the game binary. The mod opens no network
34
+ connection and starts no process. Only software already running on the same machine with write
35
+ access to the game folder can talk to it.
36
+
37
+ ## Install the mod
38
+
39
+ Extract the release zip into the game's install folder. It carries the path, so the files land in
40
+ `<game>\<Project>\Binaries\Win64\ue4ss\Mods\UEBridge`. `enabled.txt` in that folder starts the
41
+ mod; **no `mods.txt` edit**. `UE4SS.log` shows `[UEBridge] v1.0.0 ready` when it loaded.
42
+
43
+ `scripts\settings.lua` in the mod folder:
44
+
45
+ | Key | Default | Effect |
46
+ |---|---|---|
47
+ | `enabled` | `true` | `false` stops polling entirely |
48
+ | `poll_ms` | `250` | request file check interval |
49
+ | `allow_eval` | `true` | `false` refuses raw Lua (`eval_lua`); structured tools still work |
50
+ | `allow_writes` | `true` | `false` is read-only: `set_property`, `call_function`, `console_command` and `eval_lua` are refused |
51
+ | `bridge_dir` | unset | absolute path override for the request/response folder |
52
+
53
+ ## Install the server and point an agent at it
54
+
55
+ Any MCP client works. The server finds the running game (an exe in a `Binaries\Win64` folder with
56
+ `ue4ss\` beside it), so no path needs configuring; pass `--game-dir` or set `UE_BRIDGE_GAME_DIR` to
57
+ pin one.
58
+
59
+ **stdio** (Claude Code, Claude Desktop, Cursor, Windsurf, Codex, Continue, ...):
60
+
61
+ ```json
62
+ { "mcpServers": { "ue-bridge": { "command": "uvx", "args": ["ue-bridge"] } } }
63
+ ```
64
+
65
+ ```bash
66
+ claude mcp add ue-bridge -- uvx ue-bridge
67
+ ```
68
+
69
+ **HTTP** (one long-running server, several clients): `ue-bridge --http` serves streamable-HTTP MCP
70
+ on `http://127.0.0.1:8930/mcp`, loopback only.
71
+
72
+ ```bash
73
+ claude mcp add --transport http ue-bridge http://127.0.0.1:8930/mcp
74
+ ```
75
+
76
+ **Shell**, for scripts and for testing the channel:
77
+
78
+ ```bash
79
+ ue-bridge status
80
+ ue-bridge hello
81
+ ue-bridge eval "return UEB.world()"
82
+ ue-bridge props first:PlayerController
83
+ ue-bridge types ^Narrative
84
+ ```
85
+
86
+ ## Tools
87
+
88
+ | Tool | Does |
89
+ |---|---|
90
+ | `bridge_status` | Game found, running, mod answering, mod version and permissions, round-trip time. Call first. |
91
+ | `eval_lua(code, timeout)` | Any Lua chunk on the game thread. Whole UE4SS API plus the `UEB` helpers in scope; `print` output captured. |
92
+ | `world_info` | World, player controller, pawn, game instance, game mode. |
93
+ | `find_object(path)` / `find_objects(class, limit)` | Resolve one reference / list live instances of a class. |
94
+ | `list_types(pattern, limit)` | Loaded reflected types matching a Lua pattern. |
95
+ | `inspect_object(ref, include_super, pattern)` | Every reflected property with its value. |
96
+ | `list_functions(ref)` | Every UFunction on the class chain. |
97
+ | `get_property` / `set_property` | One property. `set` returns `{previous, current}`. |
98
+ | `call_function(ref, fn, args)` | Call a UFunction with positional args. |
99
+ | `console_command(cmd)` | Run a console command. |
100
+ | `batch(calls)` | Several of the above in one round trip. |
101
+ | `dump(kind)` | UE4SS dumpers: `usmap`, `jmap`, `uht`, `cxx`, `actors`, `objects`, `static_meshes`. |
102
+
103
+ Every tool except `eval_lua` goes through the structured `batch` op, so they keep working when a
104
+ user turns `allow_eval` off.
105
+
106
+ **Object references**: `/Script/Pkg.Object` (any full path), `first:ShortClassName` (first live
107
+ instance), `cdo:/Script/Pkg.Class` (class default object).
108
+
109
+ **Serialisation**: UObjects become `{"__object": fullname, "address": n}`, `FName`/`FString`/`FText`
110
+ become strings, `TArray` becomes a list (first 200), structs are walked through their reflected
111
+ type including inherited fields. `SoftObjectProperty` values are skipped by default (reading one
112
+ has hard-crashed a game inside UE4SS's own property reader).
113
+
114
+ ## Wire protocol (1)
115
+
116
+ ```
117
+ request : {"id": str, "op": "hello"|"ping"|"eval"|"batch", "code": str, "calls": [ {op, ...} ]}
118
+ response: {"id", "ok": bool, "result", "output": [str], "error": str|null, "ms": int, "protocol": 1}
119
+ ```
120
+
121
+ `hello` returns the mod's version, protocol and permissions; the server refuses to proceed on a
122
+ protocol mismatch. Anything that can write a JSON file can be a client.
123
+
124
+ ## Failure modes
125
+
126
+ - **Request never picked up**: no game running, or the mod is not installed or is disabled.
127
+ Check for `[UEBridge] ... ready` in `UE4SS.log`.
128
+ - **Picked up, no response**: the game thread is blocked (loading screen) or the request is long.
129
+ `dump` uses a 600 s timeout for that reason. If the process is gone, the error names the
130
+ in-flight operation from `bridge\lastop.log` and the newest crash report.
131
+ - **One request at a time.** A `request.json` younger than 10 s is another client's; older is a
132
+ leftover and is reclaimed.
133
+
134
+ ## Developing
135
+
136
+ ```
137
+ uv venv --python 3.11 .venv
138
+ uv pip install --python .venv\Scripts\python.exe "mcp>=1.8,<2"
139
+ .venv\Scripts\python.exe -m ue_bridge status # from the repo root
140
+ python tools/build-release.py # dist/UEBridge-<version>.zip
141
+ ```
142
+
143
+ Edit `ue4ss/UEBridge/scripts/main.lua`, then `ue-bridge eval "return UEB.reload()"`: the mod
144
+ re-runs its source in place and retires the old poll loop, no relaunch.
145
+
146
+ MIT.
@@ -0,0 +1,130 @@
1
+ # ue-bridge
2
+
3
+ Talk to a running [UE4SS](https://github.com/UE4SS-RE/RE-UE4SS) game: inspect objects, call
4
+ functions, run Lua, while it plays. Two parts, released separately:
5
+
6
+ | Part | What | Where it goes |
7
+ |---|---|---|
8
+ | **UEBridge** (Lua mod) | Polls `ue4ss\bridge\request.json`, runs the request on the game thread, writes `response.json`. Game-agnostic. | The game: `ue4ss\Mods\UEBridge\` (Nexus / GitHub release zip) |
9
+ | **ue-bridge** (Python) | An MCP server and CLI that write those files and read the answers. Finds the running game by itself. | Your machine: `uvx ue-bridge` or `pip install ue-bridge` |
10
+
11
+ ```
12
+ AI agent / script -> ue-bridge (MCP stdio or HTTP, or CLI) -> request.json
13
+ UEBridge mod: ExecuteInGameThread
14
+ ue-bridge <- response.json <-
15
+ ```
16
+
17
+ No sockets in the game, no admin rights, no changes to the game binary. The mod opens no network
18
+ connection and starts no process. Only software already running on the same machine with write
19
+ access to the game folder can talk to it.
20
+
21
+ ## Install the mod
22
+
23
+ Extract the release zip into the game's install folder. It carries the path, so the files land in
24
+ `<game>\<Project>\Binaries\Win64\ue4ss\Mods\UEBridge`. `enabled.txt` in that folder starts the
25
+ mod; **no `mods.txt` edit**. `UE4SS.log` shows `[UEBridge] v1.0.0 ready` when it loaded.
26
+
27
+ `scripts\settings.lua` in the mod folder:
28
+
29
+ | Key | Default | Effect |
30
+ |---|---|---|
31
+ | `enabled` | `true` | `false` stops polling entirely |
32
+ | `poll_ms` | `250` | request file check interval |
33
+ | `allow_eval` | `true` | `false` refuses raw Lua (`eval_lua`); structured tools still work |
34
+ | `allow_writes` | `true` | `false` is read-only: `set_property`, `call_function`, `console_command` and `eval_lua` are refused |
35
+ | `bridge_dir` | unset | absolute path override for the request/response folder |
36
+
37
+ ## Install the server and point an agent at it
38
+
39
+ Any MCP client works. The server finds the running game (an exe in a `Binaries\Win64` folder with
40
+ `ue4ss\` beside it), so no path needs configuring; pass `--game-dir` or set `UE_BRIDGE_GAME_DIR` to
41
+ pin one.
42
+
43
+ **stdio** (Claude Code, Claude Desktop, Cursor, Windsurf, Codex, Continue, ...):
44
+
45
+ ```json
46
+ { "mcpServers": { "ue-bridge": { "command": "uvx", "args": ["ue-bridge"] } } }
47
+ ```
48
+
49
+ ```bash
50
+ claude mcp add ue-bridge -- uvx ue-bridge
51
+ ```
52
+
53
+ **HTTP** (one long-running server, several clients): `ue-bridge --http` serves streamable-HTTP MCP
54
+ on `http://127.0.0.1:8930/mcp`, loopback only.
55
+
56
+ ```bash
57
+ claude mcp add --transport http ue-bridge http://127.0.0.1:8930/mcp
58
+ ```
59
+
60
+ **Shell**, for scripts and for testing the channel:
61
+
62
+ ```bash
63
+ ue-bridge status
64
+ ue-bridge hello
65
+ ue-bridge eval "return UEB.world()"
66
+ ue-bridge props first:PlayerController
67
+ ue-bridge types ^Narrative
68
+ ```
69
+
70
+ ## Tools
71
+
72
+ | Tool | Does |
73
+ |---|---|
74
+ | `bridge_status` | Game found, running, mod answering, mod version and permissions, round-trip time. Call first. |
75
+ | `eval_lua(code, timeout)` | Any Lua chunk on the game thread. Whole UE4SS API plus the `UEB` helpers in scope; `print` output captured. |
76
+ | `world_info` | World, player controller, pawn, game instance, game mode. |
77
+ | `find_object(path)` / `find_objects(class, limit)` | Resolve one reference / list live instances of a class. |
78
+ | `list_types(pattern, limit)` | Loaded reflected types matching a Lua pattern. |
79
+ | `inspect_object(ref, include_super, pattern)` | Every reflected property with its value. |
80
+ | `list_functions(ref)` | Every UFunction on the class chain. |
81
+ | `get_property` / `set_property` | One property. `set` returns `{previous, current}`. |
82
+ | `call_function(ref, fn, args)` | Call a UFunction with positional args. |
83
+ | `console_command(cmd)` | Run a console command. |
84
+ | `batch(calls)` | Several of the above in one round trip. |
85
+ | `dump(kind)` | UE4SS dumpers: `usmap`, `jmap`, `uht`, `cxx`, `actors`, `objects`, `static_meshes`. |
86
+
87
+ Every tool except `eval_lua` goes through the structured `batch` op, so they keep working when a
88
+ user turns `allow_eval` off.
89
+
90
+ **Object references**: `/Script/Pkg.Object` (any full path), `first:ShortClassName` (first live
91
+ instance), `cdo:/Script/Pkg.Class` (class default object).
92
+
93
+ **Serialisation**: UObjects become `{"__object": fullname, "address": n}`, `FName`/`FString`/`FText`
94
+ become strings, `TArray` becomes a list (first 200), structs are walked through their reflected
95
+ type including inherited fields. `SoftObjectProperty` values are skipped by default (reading one
96
+ has hard-crashed a game inside UE4SS's own property reader).
97
+
98
+ ## Wire protocol (1)
99
+
100
+ ```
101
+ request : {"id": str, "op": "hello"|"ping"|"eval"|"batch", "code": str, "calls": [ {op, ...} ]}
102
+ response: {"id", "ok": bool, "result", "output": [str], "error": str|null, "ms": int, "protocol": 1}
103
+ ```
104
+
105
+ `hello` returns the mod's version, protocol and permissions; the server refuses to proceed on a
106
+ protocol mismatch. Anything that can write a JSON file can be a client.
107
+
108
+ ## Failure modes
109
+
110
+ - **Request never picked up**: no game running, or the mod is not installed or is disabled.
111
+ Check for `[UEBridge] ... ready` in `UE4SS.log`.
112
+ - **Picked up, no response**: the game thread is blocked (loading screen) or the request is long.
113
+ `dump` uses a 600 s timeout for that reason. If the process is gone, the error names the
114
+ in-flight operation from `bridge\lastop.log` and the newest crash report.
115
+ - **One request at a time.** A `request.json` younger than 10 s is another client's; older is a
116
+ leftover and is reclaimed.
117
+
118
+ ## Developing
119
+
120
+ ```
121
+ uv venv --python 3.11 .venv
122
+ uv pip install --python .venv\Scripts\python.exe "mcp>=1.8,<2"
123
+ .venv\Scripts\python.exe -m ue_bridge status # from the repo root
124
+ python tools/build-release.py # dist/UEBridge-<version>.zip
125
+ ```
126
+
127
+ Edit `ue4ss/UEBridge/scripts/main.lua`, then `ue-bridge eval "return UEB.reload()"`: the mod
128
+ re-runs its source in place and retires the old poll loop, no relaunch.
129
+
130
+ MIT.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ue-bridge"
7
+ version = "1.0.0"
8
+ description = "Talk to a running UE4SS game: inspect objects, call functions, run Lua, while it plays. An MCP server and CLI plus the UEBridge mod."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["unreal", "ue4ss", "mcp", "modding", "lua"]
13
+ classifiers = [
14
+ "Environment :: Win32 (MS Windows)",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Games/Entertainment",
18
+ ]
19
+ # 1.8 is where streamable HTTP arrived; 2.x renamed FastMCP.
20
+ dependencies = ["mcp>=1.8,<2"]
21
+
22
+ [project.scripts]
23
+ ue-bridge = "ue_bridge.server:main"
24
+
25
+ [project.urls]
26
+ Source = "https://github.com/littleRabbit94/ue-bridge"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["ue_bridge"]
30
+
31
+ [tool.hatch.build.targets.sdist]
32
+ include = ["ue_bridge", "ue4ss", "README.md", "LICENSE"]
@@ -0,0 +1,14 @@
1
+ schema = 1
2
+
3
+ [mod]
4
+ id = "ue-bridge"
5
+ name = "UEBridge"
6
+ version = "1.0.0"
7
+ author = "Rabbit"
8
+ description = "Lua side of the ue-bridge live MCP bridge; game-agnostic, the server is the ue_bridge Python package."
9
+
10
+ [[lua]]
11
+ name = "UEBridge"
12
+ source = "."
13
+ order = 400
14
+ enabled = true
@@ -0,0 +1,165 @@
1
+ -- Minimal JSON encode/decode for UEBridge. Pure Lua 5.4, no dependencies.
2
+ -- Encodes: nil, boolean, number, string, table (array if keys are 1..n, else object).
3
+ -- Non-finite numbers encode as null. Decodes the full JSON grammar including \uXXXX.
4
+
5
+ local json = {}
6
+
7
+ local escapes = { ['"'] = '\\"', ['\\'] = '\\\\', ['\b'] = '\\b', ['\f'] = '\\f',
8
+ ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t' }
9
+
10
+ local function escapeStr(s)
11
+ return '"' .. s:gsub('[%c"\\]', function(c)
12
+ return escapes[c] or string.format('\\u%04x', c:byte())
13
+ end) .. '"'
14
+ end
15
+
16
+ local function isArray(t)
17
+ local n = 0
18
+ for k in pairs(t) do
19
+ if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then return false end
20
+ n = n + 1
21
+ end
22
+ for i = 1, n do if t[i] == nil then return false end end
23
+ return true, n
24
+ end
25
+
26
+ local function encode(v, seen)
27
+ local tv = type(v)
28
+ if v == nil then return "null" end
29
+ if tv == "boolean" then return v and "true" or "false" end
30
+ if tv == "number" then
31
+ if v ~= v or v == math.huge or v == -math.huge then return "null" end
32
+ if math.type(v) == "integer" then return tostring(v) end
33
+ return string.format("%.17g", v)
34
+ end
35
+ if tv == "string" then return escapeStr(v) end
36
+ if tv == "table" then
37
+ seen = seen or {}
38
+ if seen[v] then return '"<cycle>"' end
39
+ seen[v] = true
40
+ local out = {}
41
+ local arr, n = isArray(v)
42
+ if arr then
43
+ for i = 1, n do out[i] = encode(v[i], seen) end
44
+ seen[v] = nil
45
+ return "[" .. table.concat(out, ",") .. "]"
46
+ end
47
+ local keys = {}
48
+ for k in pairs(v) do keys[#keys + 1] = k end
49
+ table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
50
+ for _, k in ipairs(keys) do
51
+ out[#out + 1] = escapeStr(tostring(k)) .. ":" .. encode(v[k], seen)
52
+ end
53
+ seen[v] = nil
54
+ return "{" .. table.concat(out, ",") .. "}"
55
+ end
56
+ return escapeStr(tostring(v))
57
+ end
58
+
59
+ function json.encode(v) return encode(v, nil) end
60
+
61
+ -- Decoder -------------------------------------------------------------------------------
62
+
63
+ local function decodeError(str, pos, msg)
64
+ error(string.format("json: %s at position %d (%s)", msg, pos, str:sub(pos, pos + 20)), 0)
65
+ end
66
+
67
+ local function skipWs(str, pos)
68
+ return str:find("[^ \t\r\n]", pos) or #str + 1
69
+ end
70
+
71
+ local decodeValue
72
+
73
+ local simpleEscapes = { b = "\b", f = "\f", n = "\n", r = "\r", t = "\t",
74
+ ['"'] = '"', ["\\"] = "\\", ["/"] = "/" }
75
+
76
+ local function decodeString(str, pos)
77
+ local out, i = {}, pos + 1
78
+ while true do
79
+ local c = str:sub(i, i)
80
+ if c == "" then decodeError(str, i, "unterminated string") end
81
+ if c == '"' then return table.concat(out), i + 1 end
82
+ if c == "\\" then
83
+ local e = str:sub(i + 1, i + 1)
84
+ if simpleEscapes[e] then
85
+ out[#out + 1] = simpleEscapes[e]
86
+ i = i + 2
87
+ elseif e == "u" then
88
+ local cp = tonumber(str:sub(i + 2, i + 5), 16)
89
+ if not cp then decodeError(str, i, "bad \\u escape") end
90
+ i = i + 6
91
+ if cp >= 0xD800 and cp <= 0xDBFF and str:sub(i, i + 1) == "\\u" then
92
+ local lo = tonumber(str:sub(i + 2, i + 5), 16)
93
+ if lo and lo >= 0xDC00 and lo <= 0xDFFF then
94
+ cp = 0x10000 + (cp - 0xD800) * 0x400 + (lo - 0xDC00)
95
+ i = i + 6
96
+ end
97
+ end
98
+ out[#out + 1] = utf8.char(cp)
99
+ else
100
+ decodeError(str, i, "bad escape")
101
+ end
102
+ else
103
+ local j = str:find('["\\]', i) or (#str + 1)
104
+ out[#out + 1] = str:sub(i, j - 1)
105
+ i = j
106
+ end
107
+ end
108
+ end
109
+
110
+ function decodeValue(str, pos)
111
+ pos = skipWs(str, pos)
112
+ local c = str:sub(pos, pos)
113
+ if c == "{" then
114
+ local obj = {}
115
+ pos = skipWs(str, pos + 1)
116
+ if str:sub(pos, pos) == "}" then return obj, pos + 1 end
117
+ while true do
118
+ pos = skipWs(str, pos)
119
+ if str:sub(pos, pos) ~= '"' then decodeError(str, pos, "expected key") end
120
+ local key; key, pos = decodeString(str, pos)
121
+ pos = skipWs(str, pos)
122
+ if str:sub(pos, pos) ~= ":" then decodeError(str, pos, "expected ':'") end
123
+ local val; val, pos = decodeValue(str, pos + 1)
124
+ obj[key] = val
125
+ pos = skipWs(str, pos)
126
+ local d = str:sub(pos, pos)
127
+ if d == "}" then return obj, pos + 1 end
128
+ if d ~= "," then decodeError(str, pos, "expected ',' or '}'") end
129
+ pos = pos + 1
130
+ end
131
+ elseif c == "[" then
132
+ local arr = {}
133
+ pos = skipWs(str, pos + 1)
134
+ if str:sub(pos, pos) == "]" then return arr, pos + 1 end
135
+ while true do
136
+ local val; val, pos = decodeValue(str, pos)
137
+ arr[#arr + 1] = val
138
+ pos = skipWs(str, pos)
139
+ local d = str:sub(pos, pos)
140
+ if d == "]" then return arr, pos + 1 end
141
+ if d ~= "," then decodeError(str, pos, "expected ',' or ']'") end
142
+ pos = pos + 1
143
+ end
144
+ elseif c == '"' then
145
+ return decodeString(str, pos)
146
+ elseif str:sub(pos, pos + 3) == "true" then return true, pos + 4
147
+ elseif str:sub(pos, pos + 4) == "false" then return false, pos + 5
148
+ elseif str:sub(pos, pos + 3) == "null" then return nil, pos + 4
149
+ else
150
+ local num = str:match("^-?%d+%.?%d*[eE]?[-+]?%d*", pos)
151
+ if not num or num == "" then decodeError(str, pos, "unexpected character") end
152
+ local n = tonumber(num)
153
+ if not n then decodeError(str, pos, "bad number") end
154
+ return n, pos + #num
155
+ end
156
+ end
157
+
158
+ function json.decode(str)
159
+ local v, pos = decodeValue(str, 1)
160
+ pos = skipWs(str, pos)
161
+ if pos <= #str then decodeError(str, pos, "trailing garbage") end
162
+ return v
163
+ end
164
+
165
+ return json