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.
- ue_bridge-1.0.0/.gitignore +4 -0
- ue_bridge-1.0.0/LICENSE +21 -0
- ue_bridge-1.0.0/PKG-INFO +146 -0
- ue_bridge-1.0.0/README.md +130 -0
- ue_bridge-1.0.0/pyproject.toml +32 -0
- ue_bridge-1.0.0/ue4ss/UEBridge/mod.toml +14 -0
- ue_bridge-1.0.0/ue4ss/UEBridge/scripts/json.lua +165 -0
- ue_bridge-1.0.0/ue4ss/UEBridge/scripts/main.lua +674 -0
- ue_bridge-1.0.0/ue4ss/UEBridge/scripts/settings.lua +21 -0
- ue_bridge-1.0.0/ue_bridge/__init__.py +3 -0
- ue_bridge-1.0.0/ue_bridge/__main__.py +3 -0
- ue_bridge-1.0.0/ue_bridge/server.py +570 -0
ue_bridge-1.0.0/LICENSE
ADDED
|
@@ -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.
|
ue_bridge-1.0.0/PKG-INFO
ADDED
|
@@ -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
|