Compare commits

..

25 Commits

Author SHA1 Message Date
d737879ed6 More slash command stuff 2026-05-26 18:42:48 -04:00
46051526e6 New slash command parser 2026-05-26 18:20:35 -04:00
8dab0d16b7 CPL command 2026-05-26 15:07:42 -04:00
933c1ac6c3 More tweaks on tracebacks 2026-05-21 19:40:30 -04:00
521d4726ad Make lua tracebacks a little better 2026-05-21 19:24:11 -04:00
2bfa3024f1 Lots of work on the lua read-eval-print loop 2026-05-21 18:41:09 -04:00
f7983b1f02 Final form of radial menus 2026-05-19 02:55:31 -04:00
36ec4a3b9b Add new input device processor 2026-05-18 19:26:11 -04:00
1c2be1b4d8 Radial menus are in good shape 2026-05-18 16:12:26 -04:00
0b23c82e73 Progress on radial menus 2026-05-18 02:26:28 -04:00
b1defd821b More work on radial menus 2026-05-16 02:27:18 -04:00
e17f5417f2 Real progress on radial menus 2026-05-16 01:49:26 -04:00
c0848c2670 Completing downgrade to 5.5.4 2026-05-15 19:37:34 -04:00
94e6385f14 Downgrading to Unreal 5.5.4 2026-05-15 18:28:41 -04:00
1328f6e5f7 Working on radial menus 2026-05-15 18:14:38 -04:00
5d2377df1d More work on the argv conversion of ue-wingman 2026-05-13 22:03:19 -04:00
e0d45cc1db Refactoring ue-wingman to be a command-line only tool 2026-05-13 21:36:40 -04:00
ff9c045c8e Initial code for radial rendering 2026-05-11 00:13:30 -04:00
e669140e2c Layout code for radial menu complete. 2026-05-09 03:16:41 -04:00
420ea088d7 More work on radial menus 2026-05-08 17:55:11 -04:00
b00ec49e91 A few tweaks for the latest engine version 2026-05-08 14:16:52 -04:00
1aa888ac82 THinking about radial menus 2026-05-08 04:26:54 -04:00
236693fca6 More work on Prompt Widget 2026-05-07 04:11:37 -04:00
b5e121f884 Update to latest engine 2026-05-05 19:12:03 -04:00
ac4302141c Add gitdeps cache 2026-05-05 18:19:30 -04:00
100 changed files with 5189 additions and 1544 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ UnrealEngine
*.vcproj
.ignore
.gitdeps-cache/**
.vscode/**
Config/**
Saved/**

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"ue-wingman": {
"command": "python3",
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
}
}
}

View File

@@ -25,9 +25,16 @@
- `Docs/` — Documentation.
- `Config/` — Unreal config files
- `EnginePatches/` — Custom engine modifications
- `Plugins/UEWingman/' - An MCP that gives you control over the unreal editor.
- `Plugins/UEWingman/` - A plugin that gives you control over the unreal editor.
- `../integration.UE/` - the unreal engine source tree
## Using ue-wingman
- Drive it from bash using: ue-wingman <Command> <Arg1> <Arg2> ...
- ue-wingman Documentation_Manual
- ue-wingman Documentation_Commands
- ue-wingman Documentation_Command <specific_command>
## Coding Conventions
- Prefer early returns and `continue` to reduce nesting (never-nester style).

1498
Art/gamepad.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 65 KiB

321
Art/radial.svg Normal file
View File

@@ -0,0 +1,321 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="radial.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2.8284271"
inkscape:cx="365.75098"
inkscape:cy="372.82205"
inkscape:window-width="1920"
inkscape:window-height="1026"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132"
spacingx="0.99999999"
spacingy="0.99999999" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="120"
y="90"
id="text2926"><tspan
sodipodi:role="line"
id="tspan2924"
style="stroke-width:1;font-size:7.05555556px"
x="120"
y="90" /></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="116"
y="87"
id="text3271"><tspan
sodipodi:role="line"
id="tspan3269"
style="stroke-width:1"
x="116"
y="87">Light the furnace</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="120.365"
y="96.362228"
id="text3325"><tspan
sodipodi:role="line"
id="tspan3323"
style="stroke-width:1"
x="120.365"
y="96.362228">Replenish Fuel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="121"
y="107"
id="text3329"><tspan
sodipodi:role="line"
id="tspan3327"
style="stroke-width:1"
x="121"
y="107">Bake chicken</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="116"
y="117"
id="text3333"><tspan
sodipodi:role="line"
id="tspan3331"
style="stroke-width:1"
x="116"
y="117">Cannibalism</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="84"
y="87"
id="text3337"><tspan
sodipodi:role="line"
id="tspan3335"
style="stroke-width:1"
x="84"
y="87">Banana Bread</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="79"
y="97"
id="text3341"><tspan
sodipodi:role="line"
id="tspan3339"
style="stroke-width:1"
x="79"
y="97">Electric Eel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="79"
y="107"
id="text3345"><tspan
sodipodi:role="line"
id="tspan3343"
style="stroke-width:1"
x="79"
y="107">Engine Fire</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="84"
y="117"
id="text3349"><tspan
sodipodi:role="line"
id="tspan3347"
style="stroke-width:1"
x="84"
y="117">Radial Menu</tspan></text>
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 89.999999,84.999999 4,-10e-7"
id="path4558"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 106,84.999998 4,10e-7"
id="path4562"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 84.999999,94.999999 3,0"
id="path4564"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 112,94.999997 3,2e-6"
id="path4566"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 84.999999,105 3,0"
id="path4568"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 112,105 3,0"
id="path4570"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 89.999999,115 4,0"
id="path4572"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 106,115 4,0"
id="path4574"
sodipodi:nodetypes="cc" />
<path
id="path4576"
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 102,100.15803 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.104575 2,-4.000005 2,-4.000005 0,0 2.000004,2.89543 2.000004,4.000005 z"
sodipodi:nodetypes="ssscs" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 112,105 -5.72673,-2.38614"
id="path1147" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="M 106.27327,97.386131 112,94.999997"
id="path1149" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="M 102.52401,93.689963 106,84.999996"
id="path1151" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="M 97.475986,93.689963 93.999999,84.999996"
id="path1153" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 106,115 -3.47599,-8.68997"
id="path1155" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 93.999999,115 3.475987,-8.68997"
id="path1157" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 87.999998,105 5.726726,-2.38614"
id="path1159" />
<path
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 87.999998,94.999997 5.726726,2.386136"
id="path1161" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="path1220"
cx="102.52401"
cy="106.31003"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1274"
cx="97.475983"
cy="106.31003"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1276"
cx="93.726723"
cy="102.61386"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1278"
cx="106.27327"
cy="102.61386"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1280"
cx="106.27327"
cy="97.386131"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1282"
cx="102.52401"
cy="93.689964"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1284"
cx="90"
cy="85"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1286"
cx="93.726723"
cy="97.386131"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1290"
cx="97.475983"
cy="93.689964"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1292"
cx="110"
cy="85"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1294"
cx="115"
cy="95"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1296"
cx="115"
cy="105"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1298"
cx="110"
cy="115"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1300"
cx="90"
cy="115"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1302"
cx="85"
cy="105"
r="0.49999997" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1304"
cx="85"
cy="95"
r="0.49999997" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

304
Art/radial2.svg Normal file
View File

@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="radial2.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2"
inkscape:cx="375.25"
inkscape:cy="379.25"
inkscape:window-width="1920"
inkscape:window-height="1026"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132"
spacingx="0.99999999"
spacingy="0.99999999" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="120"
y="90"
id="text2926"><tspan
sodipodi:role="line"
id="tspan2924"
style="stroke-width:1;font-size:7.05555556px"
x="120"
y="90" /></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="67.497505"
id="text3271"><tspan
sodipodi:role="line"
id="tspan3269"
style="stroke-width:1"
x="130"
y="67.497505">Light the furnace</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="77.497505"
id="text3325"><tspan
sodipodi:role="line"
id="tspan3323"
style="stroke-width:1"
x="130"
y="77.497505">Replenish Fuel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="87.497505"
id="text3329"><tspan
sodipodi:role="line"
id="tspan3327"
style="stroke-width:1"
x="130"
y="87.497505">Bake chicken</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="97.497505"
id="text3333"><tspan
sodipodi:role="line"
id="tspan3331"
style="stroke-width:1"
x="130"
y="97.497505">Cannibalism</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="107.49751"
id="text3271-9"><tspan
sodipodi:role="line"
id="tspan3269-1"
style="stroke-width:1"
x="130"
y="107.49751">Light the furnace</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="117.49751"
id="text3325-2"><tspan
sodipodi:role="line"
id="tspan3323-7"
style="stroke-width:1"
x="130"
y="117.49751">Replenish Fuel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="127.49751"
id="text3329-0"><tspan
sodipodi:role="line"
id="tspan3327-9"
style="stroke-width:1"
x="130"
y="127.49751">Bake chicken</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="130"
y="137.4975"
id="text3333-3"><tspan
sodipodi:role="line"
id="tspan3331-6"
style="stroke-width:1"
x="130"
y="137.4975">Cannibalism</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="67.497505"
id="text3337"><tspan
sodipodi:role="line"
id="tspan3335"
style="stroke-width:1"
x="70"
y="67.497505">Banana Bread</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="77.497505"
id="text3341"><tspan
sodipodi:role="line"
id="tspan3339"
style="stroke-width:1"
x="70"
y="77.497505">Electric Eel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="87.497505"
id="text3345"><tspan
sodipodi:role="line"
id="tspan3343"
style="stroke-width:1"
x="70"
y="87.497505">Engine Fire</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="97.497505"
id="text3349"><tspan
sodipodi:role="line"
id="tspan3347"
style="stroke-width:1"
x="70"
y="97.497505">Radial Menu</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="107.49751"
id="text3337-3"><tspan
sodipodi:role="line"
id="tspan3335-6"
style="stroke-width:1"
x="70"
y="107.49751">Banana Bread</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="117.49751"
id="text3341-7"><tspan
sodipodi:role="line"
id="tspan3339-5"
style="stroke-width:1"
x="70"
y="117.49751">Electric Eel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="127.49751"
id="text3345-3"><tspan
sodipodi:role="line"
id="tspan3343-5"
style="stroke-width:1"
x="70"
y="127.49751">Engine Fire</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="70"
y="137.4975"
id="text3349-6"><tspan
sodipodi:role="line"
id="tspan3347-2"
style="stroke-width:1"
x="70"
y="137.4975">Radial Menu</tspan></text>
<path
id="path4576"
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 82.000003,188 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 2,-4.00001 2,-4.00001 0,0 2.000004,2.89544 2.000004,4.00001 z"
sodipodi:nodetypes="ssscs" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1284"
cx="125"
cy="194.5"
r="0.49999997" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-opacity:1;stroke-dasharray:none"
d="m 135,160 h 30"
id="path1524" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 93.038085,64.999999 99.999992,99.999995 106.96191,65"
id="path1528"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 99.999995,99.999995 83.295555,75"
id="path1530"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 116.70446,75 99.999995,99.999995 77.550899,84.999992"
id="path1532"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 122.44906,84.99999 99.999995,99.999995 74.863225,95"
id="path1536"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 99.999995,99.999995 74.863247,105"
id="path1538" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 99.999992,99.999995 77.550885,114.99999"
id="path1544" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 83.29551,125 99.999995,99.999995"
id="path1546" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="m 93.038045,135 6.96195,-35.000005"
id="path1548" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 106.96198,135 99.999992,99.999995"
id="path1550" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 116.70444,125 99.999995,99.999995"
id="path1552" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 122.44909,114.99999 99.999995,99.999995"
id="path1554" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 125.13655,95 99.999995,99.999995 125.13674,105"
id="path1556"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

449
Art/radial3.svg Normal file
View File

@@ -0,0 +1,449 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="radial3.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="2.8284271"
inkscape:cx="396.51013"
inkscape:cy="365.57421"
inkscape:window-width="1276"
inkscape:window-height="673"
inkscape:window-x="153"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132"
spacingx="0.99999999"
spacingy="0.99999999" />
</sodipodi:namedview>
<defs
id="defs2">
<inkscape:path-effect
effect="bspline"
id="path-effect7971"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7967"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7963"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7959"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7955"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7951"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7947"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7943"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7939"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7935"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7931"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7927"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7923"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7919"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7915"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
<inkscape:path-effect
effect="bspline"
id="path-effect7911"
is_visible="true"
lpeversion="1"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="120"
y="90"
id="text2926"><tspan
sodipodi:role="line"
id="tspan2924"
style="stroke-width:1;font-size:7.05555556px"
x="120"
y="90" /></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="67.497505"
id="text3271"><tspan
sodipodi:role="line"
id="tspan3269"
style="stroke-width:1"
x="105"
y="67.497505">Light the furnace</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="77.497505"
id="text3325"><tspan
sodipodi:role="line"
id="tspan3323"
style="stroke-width:1"
x="105"
y="77.497505">Replenish Fuel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="87.497505"
id="text3329"><tspan
sodipodi:role="line"
id="tspan3327"
style="stroke-width:1"
x="105"
y="87.497505">Bake chicken</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="97.497505"
id="text3333"><tspan
sodipodi:role="line"
id="tspan3331"
style="stroke-width:1"
x="105"
y="97.497505">Cannibalism</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="107.49751"
id="text3271-9"><tspan
sodipodi:role="line"
id="tspan3269-1"
style="stroke-width:1"
x="105"
y="107.49751">Light the furnace</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="117.49751"
id="text3325-2"><tspan
sodipodi:role="line"
id="tspan3323-7"
style="stroke-width:1"
x="105"
y="117.49751">Replenish Fuel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="127.49751"
id="text3329-0"><tspan
sodipodi:role="line"
id="tspan3327-9"
style="stroke-width:1"
x="105"
y="127.49751">Bake chicken</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="105"
y="137.4975"
id="text3333-3"><tspan
sodipodi:role="line"
id="tspan3331-6"
style="stroke-width:1"
x="105"
y="137.4975">Cannibalism</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="67.497505"
id="text3337"><tspan
sodipodi:role="line"
id="tspan3335"
style="stroke-width:1"
x="65"
y="67.497505">Banana Bread</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="77.497505"
id="text3341"><tspan
sodipodi:role="line"
id="tspan3339"
style="stroke-width:1"
x="65"
y="77.497505">Electric Eel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="87.497505"
id="text3345"><tspan
sodipodi:role="line"
id="tspan3343"
style="stroke-width:1"
x="65"
y="87.497505">Engine Fire</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="97.497505"
id="text3349"><tspan
sodipodi:role="line"
id="tspan3347"
style="stroke-width:1"
x="65"
y="97.497505">Radial Menu</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="107.49751"
id="text3337-3"><tspan
sodipodi:role="line"
id="tspan3335-6"
style="stroke-width:1"
x="65"
y="107.49751">Banana Bread</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="117.49751"
id="text3341-7"><tspan
sodipodi:role="line"
id="tspan3339-5"
style="stroke-width:1"
x="65"
y="117.49751">Electric Eel</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="127.49751"
id="text3345-3"><tspan
sodipodi:role="line"
id="tspan3343-5"
style="stroke-width:1"
x="65"
y="127.49751">Engine Fire</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
x="65"
y="137.4975"
id="text3349-6"><tspan
sodipodi:role="line"
id="tspan3347-2"
style="stroke-width:1"
x="65"
y="137.4975">Radial Menu</tspan></text>
<path
id="path4576"
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
d="m 82.000003,188 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 2,-4.00001 2,-4.00001 0,0 2.000004,2.89544 2.000004,4.00001 z"
sodipodi:nodetypes="ssscs" />
<circle
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
id="circle1284"
cx="125"
cy="194.5"
r="0.49999997" />
<path
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-opacity:1;stroke-dasharray:none"
d="m 135,160 h 30"
id="path1524" />
<path
sodipodi:type="star"
style="fill:none;stroke:#700726;stroke-width:1;stroke-linecap:round"
id="path9423"
inkscape:flatsided="false"
sodipodi:sides="8"
sodipodi:cx="84.999992"
sodipodi:cy="50"
sodipodi:r1="10"
sodipodi:r2="10"
sodipodi:arg1="1.5707963"
sodipodi:arg2="1.9634954"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 84.999993,60 81.173158,59.238795 77.928925,57.071068 75.761197,53.826834 74.999992,50 75.761197,46.173166 77.928924,42.928932 81.173158,40.761205 84.999992,40 l 3.826835,0.761205 3.244233,2.167727 2.167728,3.244234 L 94.999992,50 l -0.761204,3.826834 -2.167728,3.244234 -3.244233,2.167727 z"
inkscape:transform-center-x="1.0036851e-06"
transform="rotate(11.25,-168.82926,75.000033)" />
<path
style="fill:none;stroke:#700726;stroke-width:1;stroke-linecap:round"
d="M 64.999999,64.999999 H 105 V 74.999997 84.999998 94.999997 105 v 10 10 10 H 64.999999 V 125 115 105 94.999997 84.999998 74.999997 64.999999"
id="path9425" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -84,3 +84,16 @@ DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.Defaul
-ConsoleKeys=Tilde
+ConsoleKeys=Tilde
[InputPlatformSettings_Linux InputPlatformSettings]
MaxPlatformUserCount=8
input.DeviceMappingPolicy=1
MaxTriggerFeedbackPosition=8
MaxTriggerFeedbackStrength=8
MaxTriggerVibrationTriggerPosition=9
MaxTriggerVibrationFrequency=255
MaxTriggerVibrationAmplitude=8
+HardwareDevices=(InputClassName="",HardwareDeviceIdentifier="",PrimaryDeviceType=Unspecified,SupportedFeaturesMask=0)
+HardwareDevices=(InputClassName="DefaultKeyboardAndMouse",HardwareDeviceIdentifier="KBM",PrimaryDeviceType=KeyboardAndMouse,SupportedFeaturesMask=3)
+HardwareDevices=(InputClassName="DefaultGamepad",HardwareDeviceIdentifier="Gamepad",PrimaryDeviceType=Gamepad,SupportedFeaturesMask=4)
+HardwareDevices=(InputClassName="DefaultMobileTouch",HardwareDeviceIdentifier="MobileTouch",PrimaryDeviceType=Touch,SupportedFeaturesMask=8)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Widgets/teardrop.uasset LFS Normal file

Binary file not shown.

BIN
Content/Widgets/white-dot.uasset LFS Normal file

Binary file not shown.

View File

@@ -1,20 +1,12 @@
* UE Wingman rename functions.
* ue Wingman 'structprop' doesn't work for UWingXXXRef types, or for Widget slots. It needs to be implemented on top of getdetails.
* In the console, do not allow multi-line lua expressions unless it's something that reasonably should be multi-line, like a function definition or an if-statement.
* Keyboard Event Handling
* Menus
* Add a slash-command to reload lua source code.
* Skeletal Mesh Tangible
* Implement Interactive Temporary Variables
* A better text console
* Get rid of 3x3 Gridpanel stuff
* Object-Oriented Lua Support

View File

@@ -43,6 +43,35 @@
// If we're rendering offscreen, use the "dummy" SDL video driver
if (FParse::Param(FCommandLine::Get(), TEXT("RenderOffScreen")) && !getenv("SDL_VIDEODRIVER"))
{
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp.orig 2026-04-12 22:58:34.538334467 -0400
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp 2026-04-12 22:48:15.848291098 -0400
@@ -235,7 +235,26 @@
// The SDL window doesn't need to be reshaped.
// the size of the window you input is the sizeof the client.
+
+ // Under XWayland, non-override-redirect popup windows don't receive input
+ // events from the compositor. SDL already sets override_redirect for
+ // tooltips and popup menus, but other borderless child windows (like
+ // notification popups and dialogs) also need it. We temporarily enable
+ // the SDL hint to force override_redirect for these windows.
+ bool bForceOverrideRedirect = !Definition->HasOSWindowBorder
+ && InParent.IsValid()
+ && !(WindowStyle & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU));
+ if (bForceOverrideRedirect)
+ {
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "1");
+ }
+
HWnd = SDL_CreateWindow( TCHAR_TO_ANSI( *Definition->Title ), X, Y, ClientWidth, ClientHeight, WindowStyle );
+
+ if (bForceOverrideRedirect)
+ {
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "0");
+ }
// produce a helpful message for common driver errors
if (HWnd == nullptr)
{
--- Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp.orig 2026-04-12 22:58:34.349328955 -0400
+++ Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp 2026-04-12 23:05:56.395187515 -0400
@@ -15,6 +15,7 @@
@@ -50,7 +79,7 @@
#include "HAL/PlatformProcess.h"
#include "HAL/PlatformTime.h"
+#include "Modules/ModuleManager.h"
#include "AutoRTFM.h"
#include "AutoRTFM/AutoRTFM.h"
#include <link.h>
@@ -1060,3 +1061,69 @@

View File

@@ -0,0 +1,286 @@
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-05-08 14:11:02.262757499 -0400
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-05-05 15:36:35.232152395 -0400
@@ -149,7 +149,7 @@
FString SolutionDir = GetSolutionPath();
TArray<FString> Args;
Args.Add(MakePath(SolutionDir));
- Args.Add(TEXT("-g ") + MakePath(FullPath) + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber));
+ Args.Add(TEXT("-g ") + MakePath(FullPath + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber)));
return Launch(Args);
}
--- Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp.orig 2026-05-08 14:11:02.370759731 -0400
+++ Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp 2026-05-05 15:36:35.232353795 -0400
@@ -557,7 +557,7 @@
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
-#if PLATFORM_WINDOWS
+#if PLATFORM_WINDOWS || PLATFORM_LINUX
FString SourceFileName;
uint32 SourceLineNumber = 1;
uint32 SourceColumnNumber = 0;
@@ -716,8 +716,8 @@
}
UE_LOG(LogSelectionDetails, Warning, TEXT("NavigateToFunctionSource: Unable to look up symbol: %s in module:%s"), *FunctionSymbolName, *FunctionModuleName);
-
-#endif // PLATFORM_WINDOWS
+
+#endif // PLATFORM_WINDOWS || PLATFORM_LINUX
}
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp.orig 2026-05-08 14:11:02.477761942 -0400
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp 2026-05-05 15:36:35.232635087 -0400
@@ -317,6 +317,9 @@
// Furthermore SDL hides the mouse which we prevent by setting SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE, "1"); // When relative mouse mode is active, don't hide cursor.
+ // Unreal does its own dynamic capturing, we don't need SDL to do it.
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
+
// If we're rendering offscreen, use the "dummy" SDL video driver
if (FParse::Param(FCommandLine::Get(), TEXT("RenderOffScreen")) && !getenv("SDL_VIDEODRIVER"))
{
--- Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp.orig 2026-05-08 14:11:02.584764153 -0400
+++ Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp 2026-05-05 15:36:35.232750204 -0400
@@ -15,6 +15,7 @@
#include "HAL/ExceptionHandling.h"
#include "HAL/PlatformProcess.h"
#include "HAL/PlatformTime.h"
+#include "Modules/ModuleManager.h"
#include "AutoRTFM.h"
#include <link.h>
@@ -1063,3 +1064,69 @@
}
ReportLock.Unlock();
}
+
+bool FUnixPlatformStackWalk::GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber)
+{
+ // Find the .so path for this module.
+ FString ModulePath;
+ TArray<FModuleStatus> AllModules;
+ FModuleManager::Get().QueryModules(AllModules);
+ for (const FModuleStatus& Status : AllModules)
+ {
+ if (FPaths::GetBaseFilename(Status.FilePath) == FunctionModuleName)
+ {
+ ModulePath = Status.FilePath;
+ break;
+ }
+ }
+ if (ModulePath.IsEmpty())
+ {
+ return false;
+ }
+
+ // Debug symbols are in a separate .debug file alongside the .so.
+ FString DebugPath = FPaths::ChangeExtension(ModulePath, TEXT("debug"));
+ if (!FPaths::FileExists(DebugPath))
+ {
+ return false;
+ }
+
+ // Use lldb to look up the source file and line number.
+ // Run: lldb -b -o "image lookup -v -n ClassName::FuncName" <debug_file>
+ FString LldbParams = FString::Printf(TEXT("-b -o \"image lookup -v -n %s\" \"%s\""), *FunctionSymbolName, *DebugPath);
+ int32 ReturnCode = 0;
+ FString AllOutput;
+ FString Errors;
+ FPlatformProcess::ExecProcess(TEXT("/usr/bin/lldb"), *LldbParams, &ReturnCode, &AllOutput, &Errors);
+ if (ReturnCode != 0)
+ {
+ return false;
+ }
+
+ // Parse the LineEntry from lldb verbose output.
+ // Format: "LineEntry: [0x...-0x...): /path/to/file.cpp:132"
+ TArray<FString> Lines;
+ AllOutput.ParseIntoArrayLines(Lines);
+ for (const FString& Line : Lines)
+ {
+ FString Trimmed = Line.TrimStartAndEnd();
+ if (!Trimmed.StartsWith(TEXT("LineEntry:")))
+ continue;
+
+ int32 ParenIndex = Trimmed.Find(TEXT("): "));
+ if (ParenIndex == INDEX_NONE)
+ continue;
+ FString FileAndLine = Trimmed.Mid(ParenIndex + 3);
+
+ int32 ColonIndex;
+ if (!FileAndLine.FindLastChar(TCHAR(':'), ColonIndex))
+ continue;
+
+ OutPathname = FileAndLine.Left(ColonIndex);
+ OutLineNumber = FCString::Atoi(*FileAndLine.Mid(ColonIndex + 1));
+ OutColumnNumber = 0;
+ return true;
+ }
+
+ return false;
+}
--- Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h.orig 2026-05-08 14:11:02.692766385 -0400
+++ Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h 2026-05-05 15:36:35.232861435 -0400
@@ -24,6 +24,8 @@
static CORE_API void ThreadStackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, int32 IgnoreCount, uint32 ThreadId);
static CORE_API int32 GetProcessModuleCount();
static CORE_API int32 GetProcessModuleSignatures(FStackWalkModuleInfo *ModuleSignatures, const int32 ModuleSignaturesSize);
+
+ static CORE_API bool GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber);
};
typedef FUnixPlatformStackWalk FPlatformStackWalk;
--- Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h.orig 2026-05-08 14:11:02.802768658 -0400
+++ Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h 2026-05-08 14:09:14.778161399 -0400
@@ -265,6 +265,12 @@
/** The input device Id of the controller that can be used to find the matching ULocalPlayer */
FInputDeviceId DeviceId;
+ /** SDL gamepad type string (e.g. "ps4", "xboxone"), used as HardwareDeviceIdentifier in FInputDeviceScope */
+ FName GamepadType;
+
+ /** SDL gamepad name (human-readable device name), used as InputDeviceName in FInputDeviceScope */
+ FName GamepadName;
+
/** Store axis values from events here to be handled once per frame. */
TMap<FGamepadKeyNames::Type, float> AxisEvents;
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp.orig 2026-05-08 14:11:02.910770890 -0400
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp 2026-05-08 14:09:14.778345206 -0400
@@ -13,6 +13,13 @@
#include "IHapticDevice.h"
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
+namespace UE::LinuxInput
+{
+ static const FName InputClassName = TEXT("LinuxApplication");
+ static const FString KBMInputHardwareName = TEXT("KBM");
+ static const FString TouchInputHardwareName = TEXT("MobileTouch");
+}
+
//
// GameController thresholds
//
@@ -320,6 +327,7 @@
{
case SDL_EVENT_KEY_DOWN:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
const SDL_KeyboardEvent &KeyEvent = Event.key;
SDL_Keycode KeySym = KeyEvent.key;
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
@@ -342,6 +350,7 @@
break;
case SDL_EVENT_KEY_UP:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
const SDL_KeyboardEvent &KeyEvent = Event.key;
const SDL_Keycode KeySym = KeyEvent.key;
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
@@ -352,6 +361,7 @@
break;
case SDL_EVENT_TEXT_INPUT:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
// Slate now gets all its text from here, I hope.
const bool bIsRepeated = false; //Event.key.repeat != 0;
const FString TextStr(UTF8_TO_TCHAR(Event.text.text));
@@ -363,6 +373,7 @@
break;
case SDL_EVENT_MOUSE_MOTION:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
SDL_MouseMotionEvent motionEvent = Event.motion;
FLinuxCursor *LinuxCursor = (FLinuxCursor*)Cursor.Get();
LinuxCursor->InvalidateCaches();
@@ -406,6 +417,7 @@
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
SDL_MouseButtonEvent buttonEvent = Event.button;
EMouseButtons::Type button;
@@ -484,6 +496,7 @@
break;
case SDL_EVENT_MOUSE_WHEEL:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
SDL_MouseWheelEvent *WheelEvent = &Event.wheel;
float Amount = (float)WheelEvent->y * fMouseWheelScrollAccel;
@@ -515,6 +528,7 @@
SDLControllerState &ControllerState = ControllerStates[caxisEvent.which];
FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerState.DeviceId);
+ FInputDeviceScope InputScope(nullptr, ControllerState.GamepadName, ControllerState.DeviceId.GetId(), ControllerState.GamepadType.ToString());
switch (caxisEvent.axis)
{
@@ -740,15 +754,17 @@
if (Button != FGamepadKeyNames::Invalid)
{
- FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerStates[cbuttonEvent.which].DeviceId);
+ SDLControllerState& ButtonControllerState = ControllerStates[cbuttonEvent.which];
+ FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ButtonControllerState.DeviceId);
+ FInputDeviceScope InputScope(nullptr, ButtonControllerState.GamepadName, ButtonControllerState.DeviceId.GetId(), ButtonControllerState.GamepadType.ToString());
if(cbuttonEvent.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN)
{
- MessageHandler->OnControllerButtonPressed(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
+ MessageHandler->OnControllerButtonPressed(Button, UserId, ButtonControllerState.DeviceId, false);
}
else
{
- MessageHandler->OnControllerButtonReleased(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
+ MessageHandler->OnControllerButtonReleased(Button, UserId, ButtonControllerState.DeviceId, false);
}
}
}
@@ -1018,6 +1034,7 @@
case SDL_EVENT_FINGER_DOWN:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is down at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
// touch events can have no window associated with them, in that case ignore (with a warning)
@@ -1053,6 +1070,7 @@
break;
case SDL_EVENT_FINGER_UP:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is up at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
// touch events can have no window associated with them, in that case ignore (with a warning)
@@ -1087,6 +1105,7 @@
break;
case SDL_EVENT_FINGER_MOTION:
{
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
// touch events can have no window associated with them, in that case ignore (with a warning)
if (LIKELY(!bWindowlessEvent))
{
@@ -1196,6 +1215,7 @@
IPlatformInputDeviceMapper& Mapper = IPlatformInputDeviceMapper::Get();
for(auto ControllerIt = ControllerStates.CreateIterator(); ControllerIt; ++ControllerIt)
{
+ FInputDeviceScope InputScope(nullptr, ControllerIt.Value().GamepadName, ControllerIt.Value().DeviceId.GetId(), ControllerIt.Value().GamepadType.ToString());
for(auto Event = ControllerIt.Value().AxisEvents.CreateConstIterator(); Event; ++Event)
{
FPlatformUserId UserId = Mapper.GetUserForInputDevice(ControllerIt.Value().DeviceId);
@@ -2075,6 +2095,9 @@
UE_LOG(LogLinux, Verbose, TEXT("Adding controller %i '%s'"), FirstUnusedIndex, UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
auto& ControllerState = ControllerStates.Add(Id);
ControllerState.Controller = Controller;
+ ControllerState.GamepadName = FName(UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
+ const char* GamepadTypeStr = SDL_GetGamepadStringForType(SDL_GetGamepadType(Controller));
+ ControllerState.GamepadType = FName(GamepadTypeStr ? UTF8_TO_TCHAR(GamepadTypeStr) : TEXT("unknown"));
FPlatformUserId UserId = FPlatformUserId::CreateFromInternalId(FirstUnusedIndex);
DeviceMapper.RemapControllerIdToPlatformUserAndDevice(FirstUnusedIndex, UserId, ControllerState.DeviceId);

View File

@@ -28,7 +28,8 @@
"files.watcherExclude": {
"[UNREALENGINE]/Engine/**": true,
"[UNREALENGINE]/Samples/**": true,
"[UNREALENGINE]/Templates/**": true
"[UNREALENGINE]/Templates/**": true,
"**/.*": true
},
"files.associations": {
"**/include/**": "cpp",
@@ -40,7 +41,7 @@
},
"editor.acceptSuggestionOnEnter": "off",
"C_Cpp.intelliSenseEngine": "disabled",
"clangd.path": "/usr/bin/clangd-15",
"clangd.path": "/usr/bin/clangd-16",
"clangd.arguments": [
"--log=verbose",
"--query-driver=/usr/bin/g++",
@@ -130,7 +131,7 @@
"settings set target.inline-breakpoint-strategy always",
"settings set target.prefer-dynamic-value no-run-target",
"process handle SIGTRAP --notify false --pass false --stop false",
"target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\""
"target stop-hook add --one-liner \"p FUnixPlatformMisc::UngrabAllInput()\""
]
}
},

View File

@@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials.
## How Does it Work?
This tool adds a command interpreter plugin to the Unreal
This tool adds a command line interpreter plugin to the Unreal
Editor. You can type commands, and the plugin in the editor
will execute them. You can actually type commands directly
from the command-line. Here's an example of what you might
see:
will execute them.
```
$ ue-wingman.py Graph_Dump Graph=/Game/Testing/BP_Test,graph:EventGraph
$ ue-wingman Graph_Dump /Game/Testing/BP_Test,graph:EventGraph
node K2Node_Event_0: Event BeginPlay
output-pins OutputDelegate
@@ -32,33 +30,30 @@ see:
output-pins OutputDelegate, DeltaSeconds
```
There are tons of commands built in: Graph_Dump,
GraphNode_Add, GraphPin_Connect,
BlueprintComponent_Add, Widget_Add, and so forth.
Using these commands, it's possible to examine and modify
blueprints, widgets, and materials.
The ue-wingman command has tons of subcommands: Graph_Dump,
GraphNode_Add, GraphPin_Connect, BlueprintComponent_Add,
Widget_Add, and so forth. Using these commands, it's
possible to examine and modify blueprints, widgets, and
materials.
But, of course, these commands aren't really intended for humans.
They're intended for an AI agent. The AI is given access to these
commands using what's called "Model Context Protocol," which is
a goofy name for "a mechanism that an AI can use to send
commands to other software."
They're intended for an AI agent.
## Why Choose this Particular Unreal Engine MCP?
## Why Choose this Particular Unreal AI Plugin?
There are a *lot* of Unreal Engine MCPs out there. Some of
There are a *lot* of Unreal Engine AI plugins out there. Some of
them are, shall we say, not carefully engineered. I'm a
reasonably skilled software engineer and I've designed
this plugin to be robust and capable of sustained development.
This MCP is also designed to be as broadly general as
possible. I've seen MCPs that claim "can create 22 different
This plugin is also designed to be as broadly general as
possible. I've seen plugins that claim "can create 22 different
kinds of graph nodes!" This makes me ask: why not just
provide the *entire catalog* of all possible graph nodes?
I've seen MCPs claim "you can edit 15 different material
I've seen plugins claim "you can edit 15 different material
expression properties!" Why not provide access to *all*
editable material expression properties? I've tried to make
every tool in this MCP as capable as possible, with as few
every tool in this plugin as capable as possible, with as few
limits as possible.
Some of the MCPs out there expose the entire Unreal API to
@@ -76,15 +71,16 @@ commands.
## Installation
There are three parts to UE Wingman:
There are two parts to UE Wingman:
* The Unreal Plugin, which does 99% of the work.
* The python program "ue-wingman.py" which a human can
use to send commands to the plugin.
* The python program "ue-wingman-mcp.py", which an AI
can use to send commands to the plugin.
* The python program "ue-wingman.py"
The python program is actually less than 100 lines of code:
all it does is package up its command line arguments, send
them to the plugin, and let the plugin do the work. Then it
prints the output.
If you build Unreal from source, the best way to install the
plugin is to drop the entire UEWingman source folder into
@@ -106,25 +102,6 @@ and no other dependencies.
To install the human version, ue-wingman.py, just drop it into
a folder on your PATH.
To install the AI version, ue-wingman-mcp.py, you have to
usually set up some config file for your AI agent. I use
Claude Code, for that, you have to create a file ".mcp.json"
in your project folder, and it needs to have this inside it:
```
{
"mcpServers": {
"ue-wingman": {
"command": "python3",
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
}
}
}
```
You can usually ask your AI agent for help creating this
config file.
## The "User Manual"
You might be interested in seeing the "user manual" for the
@@ -135,18 +112,17 @@ $ ue-wingman.py Documentation_Manual
```
Of course, you're not the intended user: your AI agent is.
When the AI agent starts up ue-wingman-mcp, it is
automatically told to read the user manual. From there, the
User Manual says, among other things, that the AI agent can
get a listing of built-in commands. You can see that too:
You should put a note into your agent's system prompt to
let it know about the ue-wingman.py command, and to let
it know that it can type ue-wingman.py Documentation_Manual.
This in turn will tell your agent about this command:
```
$ ue-wingman.py Documentation_Commands
```
With these two commands at your disposal, you'll have a better
understanding of what exactly your AI agent is doing with this
plugin, and how it all works.
Using these commands, you can learn more about what this
plugin can do.
## Fun things to Try

View File

@@ -25,7 +25,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
FString Asset;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, skip reference check and force delete"))
UPROPERTY(EditAnywhere, meta=(Description="If true, skip reference check and force delete"))
bool Force = false;
virtual void Register() override

View File

@@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring to match against asset package paths"))
UPROPERTY(EditAnywhere, meta=(Description="Substring to match against asset package paths"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
UPROPERTY(EditAnywhere, meta=(Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual void Register() override

View File

@@ -32,16 +32,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
FString GraphType;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Create a new function or macro graph in a Blueprint."));
TEXT("Create a new function or macro graph in a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can be input, output, or local."));
}
virtual void Handle() override
{
@@ -79,8 +78,7 @@ public:
// Parse and validate variables before making changes
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Create the Graph
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,

View File

@@ -27,7 +27,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
FString Interface;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, keep the function graphs as regular functions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false;
virtual void Register() override

View File

@@ -37,5 +37,6 @@ public:
if (!P) return;
WingOut::Stdout.Print(P->GetText());
WingOut::Stdout.Print(TEXT("\n"));
}
};

View File

@@ -1,60 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "Details_SetMany.generated.h"
UCLASS()
class UWing_Details_SetMany : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target object"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Description="Object mapping property names to new values in Unreal text format"))
FWingJsonObject Properties;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Set one or more editable properties. Values use Unreal text format."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
if (!Properties.Json || Properties.Json->Values.Num() == 0)
{
WingOut::Stdout.Print(TEXT("Error: No properties specified\n"));
return;
}
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, true);
// Validation pass — resolve all properties before modifying anything.
for (const auto& Pair : Properties.Json->Values)
{
FWingProperty* P = WingUtils::FindOneWithExternalID(Pair.Key, Props, TEXT("Property"), WingOut::Stdout);
if (!P) return;
}
// Assignment pass — store the values.
int SuccessCount = 0;
for (const auto& Pair : Properties.Json->Values)
{
FWingProperty* P = WingUtils::FindOneWithExternalID(Pair.Key, Props, TEXT("Property"), WingOut::Stdout);
if (P->SetJson(*Pair.Value, WingOut::Stdout)) SuccessCount++;
}
WingOut::Stdout.Printf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
}
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include "CoreMinimal.h"
#include "WingBasics.h"
#include "WingServer.h"
#include "WingManual.h"
#include "Documentation_Command.generated.h"
UCLASS()
class UWing_Documentation_Command : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
FString Command;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Detailed documentation for one or more commands."));
}
virtual void Handle() override
{
WingManual::Commands(EWingHandlerKind::Normal, Command, true);
}
};

View File

@@ -12,19 +12,13 @@ class UWing_Documentation_Commands : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("List all the main commands with their descriptions."));
TEXT("A concise list of all ue-wingman commands."));
}
virtual void Handle() override
{
WingManual::Commands(EWingHandlerKind::Normal, Query, Verbose);
WingManual::Commands(EWingHandlerKind::Normal, TEXT(""), false);
}
};

View File

@@ -12,10 +12,10 @@ class UWing_Documentation_CreateAssets : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override

View File

@@ -12,41 +12,21 @@ class UWing_Documentation_Manual : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual"))
FString Section;
virtual void Register() override
{
TStringBuilder<128> Docs;
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
UWingServer::AddHandler(this, Docs.ToString());
UWingServer::AddHandler(this, TEXT("Print the entire manual."));
}
virtual void Handle() override
{
TSet<FName> Sections = WingManual::GetSections();
if (Section.IsEmpty())
{
UWingManualSections::FetcherPaths();
UWingManualSections::ExpressingTypes();
UWingManualSections::VariableDeclarations();
UWingManualSections::EscapeSequencesInFNames();
UWingManualSections::MaterialEditing();
UWingManualSections::ImportantCommands();
}
else
{
FName SectionName(*Section);
if (WingManual::PrintSection(SectionName))
{
WingOut::Stdout.Printf(TEXT("\n"));
WingManual::PrintSectionNames(TEXT("Other manual sections:"), Sections, WingOut::Stdout);
}
else
{
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"));
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
}
}
UWingManualSections::FetcherPaths();
UWingManualSections::ExpressingTypes();
UWingManualSections::VariableDeclarations();
UWingManualSections::EscapeSequencesInFNames();
UWingManualSections::MaterialEditing();
UWingManualSections::NodeContextMenus();
UWingManualSections::VariableGettersAndSetters();
UWingManualSections::BestPerformance();
UWingManualSections::ImportantCommands();
}
};

View File

@@ -0,0 +1,39 @@
#pragma once
#include "CoreMinimal.h"
#include "WingBasics.h"
#include "WingManual.h"
#include "WingServer.h"
#include "Documentation_Section.generated.h"
UCLASS()
class UWing_Documentation_Section : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Manual section"))
FString Section;
virtual void Register() override
{
TStringBuilder<128> Docs;
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
UWingServer::AddHandler(this, Docs.ToString());
}
virtual void Handle() override
{
FName SectionName(*Section);
if (WingManual::PrintSection(SectionName))
{
WingOut::Stdout.Printf(TEXT("\n"));
WingManual::PrintSectionNames(TEXT("Other manual sections:"), WingManual::GetSections(), WingOut::Stdout);
}
else
{
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), WingManual::GetSections(), WingOut::Stdout);
}
}
};

View File

@@ -28,13 +28,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
FString Dispatcher;
UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add a new event dispatcher to a Blueprint."));
TEXT("Add a new event dispatcher to a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can only be 'input'."));
}
virtual void Handle() override
{
@@ -49,7 +52,7 @@ public:
// Parse the arguments.
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Add the delegate variable
FEdGraphPinType DelegateType;

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "WingGraphActions.h"
#include "WingGraphExport.h"
@@ -16,24 +15,6 @@
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Type;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
FWingGraphAction *Action;
};
UCLASS()
class UWing_GraphNode_Add : public UWingHandler
{
@@ -43,8 +24,14 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {Type, posX, posY} objects. Use GraphNode_SearchTypes to find types."))
FWingJsonArray Nodes;
UPROPERTY(EditAnywhere, meta=(Description="Node type, from GraphNode_SearchTypes"))
FString Type;
UPROPERTY(EditAnywhere, meta=(Description="Node X position"))
int32 PosX = 0;
UPROPERTY(EditAnywhere, meta=(Description="Node Y position"))
int32 PosY = 0;
virtual void Register() override
{
@@ -58,38 +45,19 @@ public:
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num();
FWingGraphActions GraphActions(TargetGraph);
TArray<FWingGraphAction*> Results = GraphActions.Search(Type, 2, true);
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Type, WingOut::Stdout)) return;
// Parse the json array, turning it into an array of spawn node entries.
TArray<FSpawnNodeEntry> Entries;
FSpawnNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(nullptr, &Entry, FSpawnNodeEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(PosX, PosY));
if (NewNode)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;
TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.Type, 2, true);
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Entry.Type, WingOut::Stdout)) return;
Entry.Action = Results[0];
Entries.Add(Entry);
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Type);
WingGraphExport Export(NewNode, false, true);
WingOut::Stdout.Print(Export.GetOutput());
return;
}
// Execute all.
for (const FSpawnNodeEntry &Entry : Entries)
{
UEdGraphNode* NewNode = Entry.Action->Execute(FVector2D(Entry.PosX, Entry.PosY));
if (NewNode)
{
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Entry.Type);
WingGraphExport Export(NewNode, false, true);
WingOut::Stdout.Print(Export.GetOutput());
}
else
{
WingOut::Stdout.Printf(TEXT("Failed: %s\n\n"), *Entry.Type);
continue;
}
}
WingOut::Stdout.Printf(TEXT("Failed: %s\n"), *Type);
}
};

View File

@@ -21,7 +21,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View File

@@ -19,15 +19,15 @@ class UWing_GraphNode_SearchTypes : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain * wildcards"))
FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Query strings; each may contain * wildcards"))
FWingRestOfArgv Queries;
virtual void Register() override
{
UWingServer::AddHandler(this,
@@ -40,22 +40,8 @@ public:
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
FWingGraphActions GraphActions(TargetGraph);
for (const FString& Query : QueryStrings)
for (const FString& Query : Queries.Argv)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);

View File

@@ -4,36 +4,15 @@
#include "WingBasics.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "GraphNode_SetDefaults.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetNodeDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString Name;
UPROPERTY()
FString Value;
};
#include "GraphNode_SetDefault.generated.h"
UCLASS()
class UWing_GraphNode_SetDefaults : public UWingHandler
class UWing_GraphNode_SetDefault : public UWingHandler
{
GENERATED_BODY()
@@ -41,8 +20,14 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {node, name, value} objects"))
FWingJsonArray Pins;
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Description="Pin or property name"))
FString Name;
UPROPERTY(EditAnywhere, meta=(Description="New default value"))
FString Value;
virtual void Register() override
{
@@ -53,15 +38,15 @@ public:
// K2 graphs: set pin default values.
// -----------------------------------------------------------------------
void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema)
void HandleK2(UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema)
{
WingFetcher F(GraphObj, WingOut::Stdout);
UWingGraphPinRef* PinRef = F.Node(Entry.Node).Pin(Entry.Name).Cast<UWingGraphPinRef>();
UWingGraphPinRef* PinRef = F.Node(Node).Pin(Name).Cast<UWingGraphPinRef>();
if (!PinRef) return;
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
if (!Pin) return;
UEdGraphNode* Node = Pin->GetOwningNode();
UEdGraphNode* FoundNode = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
@@ -72,34 +57,34 @@ public:
FString UseDefaultValue;
TObjectPtr<UObject> UseDefaultObject = nullptr;
FText UseDefaultText;
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, FoundNode, Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
if (!Error.IsEmpty())
{
WingOut::Stdout.Printf(TEXT("error: %s: %s\n"), *WingUtils::FormatName(Pin), *Error);
return;
}
UWingServer::AddTouchedObject(Node);
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
UWingServer::AddTouchedObject(FoundNode);
K2Schema->TrySetDefaultValue(*Pin, Value);
}
// -----------------------------------------------------------------------
// Material graphs: set material expression properties.
// -----------------------------------------------------------------------
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj)
void HandleMaterial(UEdGraph* GraphObj)
{
WingFetcher F(GraphObj, WingOut::Stdout);
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) return;
UEdGraphNode* FoundNode = F.Node(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
TArray<FWingProperty> All = FWingProperty::GetDetails(Node, true);
FWingProperty *P = WingUtils::FindOneWithExternalID(Entry.Name, All, TEXT("Property"), WingOut::Stdout);
TArray<FWingProperty> All = FWingProperty::GetDetails(FoundNode, true);
FWingProperty *P = WingUtils::FindOneWithExternalID(Name, All, TEXT("Property"), WingOut::Stdout);
if (!P) return;
UWingServer::AddTouchedObject(Node);
UWingServer::AddTouchedObject(FoundNode);
if (!P->SetText(Entry.Value, WingOut::Stdout))
if (!P->SetText(Value, WingOut::Stdout))
return;
}
@@ -122,14 +107,8 @@ public:
return;
}
FSetNodeDefaultEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(nullptr, &Entry, FSetNodeDefaultEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *PinVal, false, WingOut::Stdout)) continue;
if (K2Schema) HandleK2Entry(Entry, GraphObj, K2Schema);
else if (MGSchema) HandleMaterialEntry(Entry, GraphObj);
}
if (K2Schema) HandleK2(GraphObj, K2Schema);
else if (MGSchema) HandleMaterial(GraphObj);
WingOut::Stdout.Printf(TEXT("Done.\n"));
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_SetPosition.generated.h"
UCLASS()
class UWing_GraphNode_SetPosition : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Description="New X position"))
int32 X = 0;
UPROPERTY(EditAnywhere, meta=(Description="New Y position"))
int32 Y = 0;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Reposition a node in a Blueprint graph."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
WingFetcher FN(TargetGraph, WingOut::Stdout);
UEdGraphNode* FoundNode = FN.Node(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
FoundNode->NodePosX = X;
FoundNode->NodePosY = Y;
WingOut::Stdout.Print(TEXT("Moved node.\n"));
}
};

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_SetPositions.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FMoveNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
int32 X = 0;
UPROPERTY()
int32 Y = 0;
};
UCLASS()
class UWing_GraphNode_SetPositions : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {node, x, y} objects"))
FWingJsonArray Nodes;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Reposition one or more nodes in a Blueprint graph."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
FMoveNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(nullptr, &Entry, FMoveNodeEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) continue;
WingFetcher FN(TargetGraph, WingOut::Stdout);
UEdGraphNode* Node = FN.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) continue;
Node->NodePosX = Entry.X;
Node->NodePosY = Entry.Y;
SuccessCount++;
}
WingOut::Stdout.Printf(TEXT("Moved %d/%d nodes.\n"), SuccessCount, Nodes.Array.Num());
}
};

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
@@ -12,23 +11,6 @@
#include "GraphPin_Connect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS()
class UWing_GraphPin_Connect : public UWingHandler
{
@@ -38,8 +20,8 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {sourcePin, targetPin} objects"))
FWingJsonArray Connections;
UPROPERTY(EditAnywhere, meta=(Description="Alternating source pin / target pin strings"))
FWingRestOfArgv SourcePin_TargetPin;
virtual void Register() override
{
@@ -54,24 +36,27 @@ public:
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalCount = Connections.Array.Num();
FConnectPinsEntry Entry;
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(nullptr, &Entry, FConnectPinsEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
if ((SourcePin_TargetPin.Argv.Num() % 2) != 0)
{
if (!FWingProperty::PopulateFromJson(EntryProps, *ConnVal, false, WingOut::Stdout))
continue;
WingOut::Stdout.Print(TEXT("ERROR: SourcePin_TargetPin must contain an even number of arguments.\n"));
return;
}
int32 SuccessCount = 0;
int32 TotalCount = SourcePin_TargetPin.Argv.Num() / 2;
for (int32 I = 0; I < SourcePin_TargetPin.Argv.Num(); I += 2)
{
const FString& SourcePinPath = SourcePin_TargetPin.Argv[I];
const FString& TargetPinPath = SourcePin_TargetPin.Argv[I + 1];
WingFetcher FS(G, WingOut::Stdout);
UWingGraphPinRef* SourcePinRef = FS.Walk(Entry.SourcePin).Cast<UWingGraphPinRef>();
UWingGraphPinRef* SourcePinRef = FS.Walk(SourcePinPath).Cast<UWingGraphPinRef>();
if (!SourcePinRef) continue;
UEdGraphPin* SourcePin = WingUtils::CheckGetPin(SourcePinRef->Node, SourcePinRef->PinName, WingOut::Stdout);
if (!SourcePin) continue;
WingFetcher FT(G, WingOut::Stdout);
UWingGraphPinRef* TargetPinRef = FT.Walk(Entry.TargetPin).Cast<UWingGraphPinRef>();
UWingGraphPinRef* TargetPinRef = FT.Walk(TargetPinPath).Cast<UWingGraphPinRef>();
if (!TargetPinRef) continue;
UEdGraphPin* TargetPin = WingUtils::CheckGetPin(TargetPinRef->Node, TargetPinRef->PinName, WingOut::Stdout);
if (!TargetPin) continue;

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
@@ -24,8 +23,8 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of pin ID strings"))
FWingJsonArray Pins;
UPROPERTY(EditAnywhere, meta=(Description="Pin ID strings"))
FWingRestOfArgv Pins;
virtual void Register() override
{
@@ -43,15 +42,8 @@ public:
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
for (const FString& PinPath : Pins.Argv)
{
FString PinPath;
if (!PinVal->TryGetString(PinPath))
{
WingOut::Stdout.Print(TEXT("ERROR: Expected a string pin ID.\n"));
continue;
}
WingFetcher FP(G, WingOut::Stdout);
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
if (!PinRef) continue;
@@ -72,6 +64,6 @@ public:
}
WingOut::Stdout.Printf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
SuccessCount, Pins.Array.Num(), TotalDisconnected);
SuccessCount, Pins.Argv.Num(), TotalDisconnected);
}
};

View File

@@ -22,7 +22,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View File

@@ -1,41 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "Sequence.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Sequence : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description=
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
FWingJsonArray Subcommands;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Execute multiple commands in one request. Each subcommand "
"produces its own content block in the response. The big win "
"performance-wise is that fewer MCP calls means fewer "
"round-trip invocations of the LLM."));
}
virtual void Handle() override
{
// The actual code that implements Sequence is hardwired into
// WingServer. Because of that, this handler is never actually called
// under normal conditions. The handler exists for two reasons: to
// provide documentation, and also to catch the case where somebody
// nests a sequence inside another sequence.
WingOut::Stdout.Print(
TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
}
};

View File

@@ -20,7 +20,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results"))
int32 Limit = 100;
virtual void Register() override

View File

@@ -21,22 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add new variables. Format: 'type name (flags) = default', one per line."));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Create(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,23 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Modify variables of a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -47,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Modify(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,22 +21,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variable names to remove, comma-separated"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variable names to remove, comma-separated"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variable names to remove, comma-separated"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Remove variables from a blueprint, graph, or custom event node."));
TEXT("Remove variables from a blueprint, graph, or custom event node. "
"Each variable must be expressed as: 'kind name'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +39,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseNamesString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseNamesString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseNamesString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, true, WingOut::Stdout)) return;
if (!Vars.Remove(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));
}

View File

@@ -33,10 +33,10 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
FString Name;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Parent widget name. If omitted, sets as root."))
UPROPERTY(EditAnywhere, meta=(Description="Parent widget name. If omitted, sets as root."))
FString Parent;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Whether to expose the widget as a variable in the blueprint (default false)"))
UPROPERTY(EditAnywhere, meta=(Description="Whether to expose the widget as a variable in the blueprint (default false)"))
bool IsVariable = false;
virtual void Register() override
@@ -117,6 +117,10 @@ public:
BP->WidgetTree->RootWidget = NewWidget;
}
// Register a variable GUID for the new widget. UMG's compiler
// ensures every widget in the tree is present in this map.
// BP->OnVariableAdded(NewWidget->GetFName());
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
}
};

View File

@@ -17,12 +17,12 @@ class UWing_Widget_SearchTypes : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain *"))
FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Query strings; each may contain *"))
FWingRestOfArgv Queries;
virtual void Register() override
{
UWingServer::AddHandler(this,
@@ -31,22 +31,8 @@ public:
}
virtual void Handle() override
{
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
WingWidgets Widgets;
for (const FString& Query : QueryStrings)
for (const FString& Query : Queries.Argv)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);

View File

@@ -6,17 +6,23 @@
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
{
WingOut::Stdout.Print(TEXT("ue-wingman "));
WingOut::Stdout.Print(Handler.Name);
WingOut::Stdout.Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) WingOut::Stdout.Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?"));
WingOut::Stdout.Print(PropIt->GetName());
FStructProperty* StructProp = CastField<FStructProperty>(*PropIt);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
if (bIsRest)
{
WingOut::Stdout.Printf(TEXT(" [%s...]"), *PropIt->GetName());
}
else
{
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
}
}
WingOut::Stdout.Print(TEXT(")\n"));
WingOut::Stdout.Print(TEXT("\n"));
}
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
@@ -26,197 +32,206 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
{
FProperty* Prop = *PropIt;
FString Name = Prop->GetName();
FString Type = UWingTypes::TypeToText(Prop);
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
FString Desc = Prop->GetMetaData(TEXT("Description"));
if (Desc.IsEmpty()) Desc = TEXT("No documentation");
if (bOptional)
{
WingOut::Stdout.Printf(TEXT(" %s (optional %s)"), *Name, *Type);
}
else
{
WingOut::Stdout.Printf(TEXT(" %s (%s)"), *Name, *Type);
}
if (!Desc.IsEmpty()) WingOut::Stdout.Printf(TEXT(" — %s"), *Desc);
WingOut::Stdout.Print(TEXT("\n"));
WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc);
}
}
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
{
if (Handler.Documentation.IsEmpty()) return;
WingOut::Stdout.Print(WingUtils::WrapText(Handler.Documentation, 80, TEXT(" // ")));
WingOut::Stdout.Printf(TEXT("\n%s\n\n"), *Handler.Documentation);
}
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
{
WingOut::Stdout.Print(TEXT("\n"));
PrintHandlerPrototype(Handler);
PrintHandlerArguments(Handler);
PrintHandlerDescription(Handler);
PrintHandlerPrototype(Handler);
PrintHandlerArguments(Handler);
PrintHandlerDescription(Handler);
WingOut::Stdout.Print(TEXT("\n"));
}
void UWingManualSections::FetcherPaths()
{
WingOut::Stdout.Print(TEXT(
"\n FETCHER PATHS:"
"\n"
"\n Most commands require you to specify a 'fetcher path'."
"\n A fetcher path starts with an asset name, followed by"
"\n steps that navigate into the asset. Some Examples:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n widget — move from a widget blueprint to a widget"
"\n structprop — move into a struct property of an object"
"\n"
"\n Notice that paths use escaped fnames. See the section"
"\n on escape sequences in fnames below sfor more information."
"\n"
"\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:"
"\n"
"\n /Game/Materials/MyMaterial,graph"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n FETCHER PATHS:"
"\n"
"\n Most commands require you to specify a 'fetcher path'."
"\n A fetcher path starts with an asset name, followed by"
"\n steps that navigate into the asset. Some Examples:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n widget — move from a widget blueprint to a widget"
"\n structprop — move into a struct property of an object"
"\n"
"\n Notice that paths use escaped fnames. See the section"
"\n on escape sequences in fnames below sfor more information."
"\n"
"\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:"
"\n"
"\n /Game/Materials/MyMaterial,graph"
"\n"
));
}
void UWingManualSections::ExpressingTypes()
{
WingOut::Stdout.Print(TEXT(
"\n EXPRESSING TYPES:"
"\n"
"\n To change the type of a variable, or to express function parameters,"
"\n you will use our syntax for types. Here are some valid examples:"
"\n"
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
"\n Array<Int>, Set<String>, Map<Int,Actor>"
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
"\n"
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
"\n is used as a type, the typename is just BP_Foo. You can search"
"\n for valid types using the TypeName_Search command."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n EXPRESSING TYPES:"
"\n"
"\n To change the type of a variable, or to express function parameters,"
"\n you will use our syntax for types. Here are some valid examples:"
"\n"
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
"\n Array<Int>, Set<String>, Map<Int,Actor>"
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
"\n"
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
"\n is used as a type, the typename is just BP_Foo. You can search"
"\n for valid types using the TypeName_Search command."
"\n"
));
}
void UWingManualSections::VariableDeclarations()
{
WingOut::Stdout.Print(TEXT(
"\n VARIABLE DECLARATIONS:"
"\n"
"\n We have our own syntax for variable declarations: a type,"
"\n a name, optional flags, and an optional default value,"
"\n always on one line:"
"\n"
"\n Array<Actor> Actors"
"\n Float F (InstanceEditable)"
"\n String S = This is the default value"
"\n"
"\n The commands Variables_Add, Variables_Modify,"
"\n and Variables_Remove can be used to edit "
"\n blueprint variables, graph local variables, graph input"
"\n variables, graph output variables, and custom"
"\n event node input variables. Event dispatchers are"
"\n also graphs, so they too can be edited."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n VARIABLE DECLARATIONS:"
"\n"
"\n We have our own syntax for variable declarations:"
"\n"
"\n kind type name (optional flags) = optional default value"
"\n"
"\n Kind can be:"
"\n"
"\n blueprint - eg, instance variables"
"\n input - a function argument or macro input"
"\n output - a function return value or macro output"
"\n local - local variables"
"\n"
"\n Here are some examples:"
"\n"
"\n input Array<Actor> Actors"
"\n output Float F (InstanceEditable)"
"\n blueprint String S = This is the default value"
"\n"
"\n The commands Variables_Add, Variables_Modify,"
"\n and Variables_Remove can be used to edit "
"\n blueprint variables, graph local variables, graph input"
"\n variables, graph output variables, and custom"
"\n event node input variables. Event dispatchers are"
"\n also graphs, so they too can be edited."
"\n"
));
}
void UWingManualSections::EscapeSequencesInFNames()
{
WingOut::Stdout.Print(TEXT(
"\n ESCAPE SEQUENCES IN FNAMES:"
"\n"
"\n When we output FNames, we use HTML escape sequences for the"
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
"\n We also translate spaces to periods."
"\n"
"\n When sending FNames to UE Wingman, you *must* escape the marks"
"\n listed above, but you *may* escape any character. To send an FName"
"\n with a space in it, either use &#32; or a period."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n ESCAPE SEQUENCES IN FNAMES:"
"\n"
"\n When we output FNames, we use HTML escape sequences for the"
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
"\n We also translate spaces to periods."
"\n"
"\n When sending FNames to UE Wingman, you *must* escape the marks"
"\n listed above, but you *may* escape any character. To send an FName"
"\n with a space in it, either use &#32; or a period."
"\n"
));
}
void UWingManualSections::MaterialEditing()
{
WingOut::Stdout.Print(TEXT(
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain"
"\n properties which actually come from the material expressions."
"\n You can edit these using Details_Set on the node."
"\n"
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
"\n a single node what would otherwise take many.\n"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain"
"\n properties which actually come from the material expressions."
"\n You can edit these using Details_Set on the node."
"\n"
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
"\n a single node what would otherwise take many.\n"
"\n"
));
}
void UWingManualSections::NodeContextMenus()
{
WingOut::Stdout.Print(TEXT(
"\n NODE CONTEXT MENUS:"
"\n"
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
"\n to the node context menu. This menu includes both node"
"\n operations and pin operations (e.g. Split Struct Pin,"
"\n Add Pin)."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n NODE CONTEXT MENUS:"
"\n"
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
"\n to the node context menu. This menu includes both node"
"\n operations and pin operations (e.g. Split Struct Pin,"
"\n Add Pin)."
"\n"
));
}
void UWingManualSections::VariableGettersAndSetters()
{
WingOut::Stdout.Print(TEXT(
"\n VARIABLE GETTERS AND SETTERS:"
"\n"
"\n Access to local vars, function parameters, and "
"\n blueprint vars is through getter and setter nodes. "
"\n These can be found in GraphNode_SearchTypes by "
"\n searching for 'Variable'. Some examples:"
"\n"
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n VARIABLE GETTERS AND SETTERS:"
"\n"
"\n Access to local vars, function parameters, and "
"\n blueprint vars is through getter and setter nodes. "
"\n These can be found in GraphNode_SearchTypes by "
"\n searching for 'Variable'. Some examples:"
"\n"
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
"\n"
));
}
void UWingManualSections::BestPerformance()
{
WingOut::Stdout.Print(TEXT(
"\n BEST PERFORMANCE:"
"\n"
"\n UE Wingman is much faster than the LLM. Therefore, it is"
"\n advantageous to batch: chain multiple ue-wingman commands"
"\n together using bash semicolon."
"\n"
));
}
void UWingManualSections::ImportantCommands()
{
WingOut::Stdout.Print(TEXT(
"\n IMPORTANT COMMANDS:"
"\n"
"\n Documentation_Manual: print manual sections"
"\n Documentation_Commands: a list of all the main commands"
"\n Documentation_CreateAssets: Additional commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n Sequence: Batch commands together for faster execution"
"\n"
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
"\n to get detailed help for a specific command."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n IMPORTANT COMMANDS:"
"\n"
"\n Documentation_Manual: print the entire manual"
"\n Documentation_Section: print a single section of the manual"
"\n Documentation_Commands: print concise list of all ue-wingman commands"
"\n Documentation_Command: detailed documentation for a single ue-wingman command"
"\n Documentation_CreateAssets: list of commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n"
));
}
TSet<FName> WingManual::GetSections()
@@ -232,7 +247,7 @@ TSet<FName> WingManual::GetSections()
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
{
if (Sections.IsEmpty()) return;
if (Prefix) Output.Print(Prefix);
if (Prefix) Output.Print(Prefix);
bool bFirst = true;
for (const FName& Section : Sections)
{
@@ -240,7 +255,7 @@ void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Secti
bFirst = false;
Output.Printf(TEXT("%s"), *Section.ToString());
}
if (Prefix) Output.Print(TEXT("\n"));
if (Prefix) Output.Print(TEXT("\n"));
}
bool WingManual::PrintSection(FName Section)
@@ -256,10 +271,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
FString QueryLower = Query.ToLower();
FString PrevGroup;
bool any = false;
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
{
if (H.Kind != Kind) continue;
if (!H.Name.ToLower().Contains(QueryLower)) continue;
any = true;
// Blank line between groups
if (!Verbose)
@@ -278,4 +295,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
else
PrintHandlerPrototype(H);
}
if (!any)
{
WingOut::Stdout.Print(TEXT("No matching commands. To see a full list, type:\n"));
WingOut::Stdout.Print(TEXT(" ue-wingman Documentation_Commands.\n"));
}
}

View File

@@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
return true;
}
bool FWingProperty::SetJson(const FJsonValue &JsonValue, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
if (JsonValue.Type == EJson::String)
{
return SetText(JsonValue.AsString(), Errors);
}
if (JsonValue.Type == EJson::Number)
{
return SetDouble(JsonValue.AsNumber(), Errors);
}
if (JsonValue.Type == EJson::Boolean)
{
return SetBool(JsonValue.AsBool(), Errors);
}
if (JsonValue.Type == EJson::Object)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonObject::StaticStruct()))
{
FWingJsonObject Val;
Val.Json = JsonValue.AsObject();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json object"), Errors);
return false;
}
if (JsonValue.Type == EJson::Array)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonArray::StaticStruct()))
{
FWingJsonArray Val;
Val.Array = JsonValue.AsArray();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json array"), Errors);
return false;
}
PrintExpectsReceived(TEXT("Unrecognized Json Data"), Errors);
return false;
}
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
{
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
@@ -493,56 +442,54 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
return Result;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors)
bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
{
bool Ok = true;
// Build a set of known property names for the unknown-field check.
TSet<FName> KnownKeys;
for (const FWingProperty& P : Props) KnownKeys.Add(P->GetFName());
// Check for unknown fields in the JSON
for (const auto& KV : Json.Values)
int32 ArgIndex = 0;
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
{
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
if (!KnownKeys.Contains(Name))
{
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
Ok = false;
}
}
FWingProperty& P = Props[PropIndex];
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
// Populate each property from JSON
for (FWingProperty& P : Props)
{
FString JsonKey = WingUtils::FormatName(P.Prop);
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
if (!Value)
if (bIsRest)
{
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
if (!Optional)
if (PropIndex + 1 != Props.Num())
{
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
Ok = false;
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
FWingRestOfArgv Rest;
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
{
Rest.Argv.Add(Argv[I]);
}
P.Prop->SetValue_InContainer(P.Container, &Rest);
ArgIndex = Argv.Num();
continue;
}
if (!P.SetJson(*Value, Errors)) Ok = false;
}
return Ok;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
{
// Make sure they passed in a JSON object.
TSharedPtr<FJsonObject> Obj = Json.AsObject();
if (Obj == nullptr)
if (ArgIndex >= Argv.Num())
{
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
ArgIndex++;
}
if (ArgIndex < Argv.Num())
{
Errors.Printf(TEXT("property data should be stored in a json object\n"));
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
*Argv[ArgIndex]);
return false;
}
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
return true;
}

View File

@@ -81,7 +81,7 @@ void UWingServer::Deinitialize()
bShuttingDown = true;
for (auto& Msg : PendingMessages)
{
Msg->Response.SetValue(FString());
Msg->Response.SetValue(TArray<uint8>());
}
PendingMessages.Empty();
}
@@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime)
// If we have a request, process it.
if (Request.IsValid())
{
FString Response = HandleRequest(Request->Line);
TArray<uint8> Response = HandleRequest(Request->Request);
Request->Response.SetValue(Response);
}
}
@@ -169,71 +169,24 @@ TStatId UWingServer::GetStatId() const
// HandleRequest — Given a command, execute it.
// ============================================================
FString UWingServer::HandleRequest(const FString& Line)
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
{
// Parse the request as JSON before doing anything else.
TSharedPtr<FJsonValue> Value;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
if (!FJsonSerializer::Deserialize(Reader, Value))
return PackageResponses({TEXT("Invalid Json")});
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
if (!Value->TryGetObject(RequestPtr))
return PackageResponses({TEXT("Json must be an object")});
TSharedPtr<FJsonObject> Request = *RequestPtr;
TArray<FString> Argv;
FString ResponseText;
FString Command;
Request->TryGetStringField(TEXT("command"), Command);
if (Command == TEXT("Sequence"))
if (DeserializeArgv(RequestBytes, Argv))
{
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
TArray<FString> Responses;
Responses.Reserve(Subcommands->Num());
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
{
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
if (!Sub->TryGetObject(SubObjPtr))
Responses.Add(TEXT("Subcommand must be a JSON object."));
else
Responses.Add(HandleJsonRequest(*SubObjPtr));
}
return PackageResponses(Responses);
PreCallHandler();
TryCallHandler(Argv);
ResponseText = PostCallHandler();
}
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
return PackageResponses({HandleJsonRequest(Request)});
FTCHARToUTF8 Utf8(*ResponseText);
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
}
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
{
TArray<TSharedPtr<FJsonValue>> Blocks;
Blocks.Reserve(Responses.Num());
for (const FString& Response : Responses)
{
// Unreal's JSON writer terminates string serialization at the first
// embedded null byte rather than escaping it, which would silently
// truncate output. Sanitize null bytes to spaces.
FString Sanitized = Response;
for (int32 i = 0; i < Sanitized.Len(); ++i)
{
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
}
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
Block->SetStringField(TEXT("type"), TEXT("text"));
Block->SetStringField(TEXT("text"), Sanitized);
Blocks.Add(MakeShared<FJsonValueObject>(Block));
}
FString OutJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
FJsonSerializer::Serialize(Blocks, Writer);
return OutJson;
}
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
void UWingServer::PreCallHandler()
{
LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true;
@@ -241,9 +194,10 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
SuggestedManualSections.Empty();
bSuggestHandlerHelp = false;
LastHandler = nullptr;
}
TryCallHandler(Request);
FString UWingServer::PostCallHandler()
{
Notifier.SendNotifications();
LogCapture.bEnabled = false;
for (const FString& Msg : LogCapture.CapturedErrors)
@@ -269,17 +223,21 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
return Result;
}
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
{
// Extract the command from the request.
FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command))
FString Command = "Documentation_Manual";
if (Argv.Num() > 0)
{
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
return;
Command = Argv[0];
Argv = Argv.RightChop(1);
}
Request->RemoveField(TEXT("command"));
if ((Command.Equals(TEXT("--help"))) ||
(Command.Equals(TEXT("-help"))) ||
(Command.Equals(TEXT("help"))))
{
Command = "Documentation_Manual";
}
// Find the handler for the specified command.
FWingHandlerConfig* Found = FindHandler(Command);
@@ -296,9 +254,9 @@ void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
Handler->Configuration = Found;
// Populate the handler object with the request parameters.
// Populate the handler object with argv parameters.
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
if (!FWingProperty::PopulateFromArgv(Props, Argv, WingOut::Stdout))
{
UWingServer::SuggestHandlerHelp();
return;
@@ -356,102 +314,105 @@ void UWingServer::CleanupFinishedClients()
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
FSocket* Socket = Client->Socket;
TArray<uint8> RecvBuf;
RecvBuf.SetNumUninitialized(MinUnusedRecvSpace);
int32 RecvLen = 0;
WaitForAssetRegistry();
while (true)
TArray<uint8> Request;
if (!ReceiveRequest(Socket, Request))
{
FString Request;
if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request))
{
FString Response;
if (!ProcessRequestOnGameThread(Request, Response))
{
Client->bDone = true;
return;
}
// Write the response back, null-terminated (blocking)
FTCHARToUTF8 Utf8(*Response);
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
Utf8.Length() + 1))
{
Client->bDone = true;
return;
}
continue;
}
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
{
break;
}
Client->bDone = true;
return;
}
TArray<uint8> Response;
if (!ProcessRequestOnGameThread(Request, Response))
{
Client->bDone = true;
return;
}
SendAll(Socket, Response.GetData(), Response.Num());
Client->bDone = true;
}
bool UWingServer::ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest)
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
{
const uint8* EndOfRequest = static_cast<const uint8*>(
memchr(RecvBuf.GetData(), '\0', RecvLen));
if (EndOfRequest == nullptr)
return
((uint32)Data[0] << 24) |
((uint32)Data[1] << 16) |
((uint32)Data[2] << 8) |
(uint32)Data[3];
}
bool UWingServer::DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
{
Argv.Empty();
int32 Offset = 0;
while (Offset < RequestBytes.Num())
{
return false;
if (RequestBytes.Num() - Offset < 4)
{
Argv.Empty();
return false;
}
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
Offset += 4;
if ((uint32)(RequestBytes.Num() - Offset) < Length)
{
Argv.Empty();
return false;
}
Argv.Add(FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
Length));
Offset += (int32)Length;
}
const int32 MessageLen =
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
OutRequest = FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
if (RemainingBytes > 0)
{
FMemory::Memmove(
RecvBuf.GetData(),
RecvBuf.GetData() + MessageLen + 1,
RemainingBytes);
}
RecvLen = RemainingBytes;
return true;
}
bool UWingServer::ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen)
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
constexpr int32 ChunkSize = 8192;
int32 UnusedSpace = RecvBuf.Num() - RecvLen;
if (UnusedSpace < MinUnusedRecvSpace)
TArray<uint8> RecvBuf;
RecvBuf.Reserve(ChunkSize);
// Unreal's FSocket API is fundamentally broken: recv cannot
// differentiate between a socket that has been cleanly closed
// and a socket that has had an error. So we have no choice
// but to just read until recv returns false (which could be a
// clean close or an error). Then, we check if we have a cleanly
// encoded payload: if so, we assume everything is fine.
while (true)
{
if (RecvBuf.Num() >= MaxRecvBufBytes)
uint8 Temp[ChunkSize];
int32 BytesRead = 0;
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
{
break;
}
if (BytesRead <= 0) break;
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
{
return false;
}
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
UnusedSpace = RecvBuf.Num() - RecvLen;
RecvBuf.Append(Temp, BytesRead);
}
int32 BytesRead = 0;
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
{
return false;
}
if (BytesRead <= 0)
{
return false;
}
if (RecvBuf.Num() < 4) return false;
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
RecvBuf.RemoveAt(0, 4);
RecvLen += BytesRead;
OutRequest = MoveTemp(RecvBuf);
return true;
}
@@ -471,13 +432,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
}
bool UWingServer::ProcessRequestOnGameThread(
const FString& Request, FString& Response)
const TArray<uint8>& Request, TArray<uint8>& Response)
{
// Enqueue the message for game-thread processing.
TSharedPtr<UWingServer::FPendingMessage> Msg =
MakeShared<UWingServer::FPendingMessage>();
Msg->Line = Request;
TFuture<FString> Future = Msg->Response.GetFuture();
Msg->Request = Request;
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
{
FScopeLock Lock(&GWingServer->Mutex);

View File

@@ -82,7 +82,7 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
{
if ((!Allow) && (!Variables.IsEmpty()))
{
Errors.Printf(TEXT("In this context, %s must be empty."), ListName);
Errors.Printf(TEXT("This object does not support %s.\n"), ListName);
return false;
}
for (const Var &Variable : Variables)
@@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
return true;
}
bool WingVariableList::ParseString(const FString &Input, WingOut Errors)
{
Variables.Empty();
TArray<FString> Lines;
Input.ParseIntoArrayLines(Lines);
for (const FString& Line : Lines)
{
WingTokenizer Tok(Line);
if (Tok.NextType() == 0) continue;
Var V;
V.DefaultSpecified = false;
if (!ParseOneVariable(Tok, V, Errors)) return false;
Variables.Add(MoveTemp(V));
}
return true;
}
bool WingVariableList::ParseNamesString(const FString &Input, WingOut Errors)
{
Variables.Empty();
WingTokenizer Tok(Input);
while (Tok.TokenIs(Tok.Identifier))
{
FName Name = Tok.NextName();
Var V;
V.Name = Name;
Variables.Add(V);
V.DefaultSpecified = false;
Tok.Advance();
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(0))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("Unexpected token %s in variable list"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors)
{
// Parse type.
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if (Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (Tok.NextType() == Tok.RestOfLine)
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
void WingVariables::Empty()
{
BlueprintVariables.Empty();
@@ -254,6 +144,107 @@ void WingVariables::Print(WingOut Out)
OutputVariables.Print(Out);
}
WingVariableList *WingVariables::GetList(FName Name)
{
if (Name == TEXT("blueprint")) return &BlueprintVariables;
if (Name == TEXT("input")) return &InputVariables;
if (Name == TEXT("output")) return &OutputVariables;
if (Name == TEXT("local")) return &LocalVariables;
return nullptr;
}
bool WingVariables::ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors)
{
// Parse Kind.
if (GetList(Tok.NextName()) == nullptr)
{
Errors.Print(TEXT("ERROR: Variable description should start with 'blueprint', 'input', 'output', or 'local'"));
return false;
}
Kind = Tok.NextName();
Tok.Advance();
// Parse type.
if (!NameOnly)
{
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
}
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if ((!NameOnly) && Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (!NameOnly && (Tok.NextType() == Tok.RestOfLine))
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariables::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
bool WingVariables::Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors)
{
for (const FString& Onevar : Vars)
{
WingTokenizer Tok(Onevar);
FName Kind;
Var V;
if (!ParseOneVariable(Tok, Kind, V, NameOnly, Errors)) return false;
WingVariableList *List = GetList(Kind);
List->Add(V);
}
return true;
}
void WingVariables::Load(WingOut Errors)
{
Empty();

View File

@@ -62,32 +62,18 @@ public:
////////////////////////////////////////////////////////////
//
// Json wrappers.
//
// Normally, the json request is automatically used to
// populate the properties of the handler, so the handler
// doesn't have to deal with json. However, in a few cases,
// the handler actually does want to see some json. These
// wrappers allow a handler to request raw json data instead
// of pre-processed values.
// A simple type to store the remaining arguments in
// an Argv Array.
//
////////////////////////////////////////////////////////////
USTRUCT()
struct FWingJsonObject
struct FWingRestOfArgv
{
GENERATED_BODY()
TSharedPtr<FJsonObject> Json;
};
// Marker struct for handler parameters that accept a JSON array.
// PopulateFromJson stashes the actual JSON array into the Array field.
//
USTRUCT()
struct FWingJsonArray
{
GENERATED_BODY()
TArray<TSharedPtr<FJsonValue>> Array;
UPROPERTY()
TArray<FString> Argv;
};
////////////////////////////////////////////////////////////
@@ -200,3 +186,4 @@ public:
bool Editable;
};

View File

@@ -48,4 +48,7 @@ public:
UFUNCTION()
static void VariableGettersAndSetters();
UFUNCTION()
static void BestPerformance();
};

View File

@@ -41,7 +41,6 @@ struct FWingProperty
bool SetInt64(int64 I, WingOut Errors) const;
bool SetBool(bool B, WingOut Errors) const;
bool SetText(FString Value, WingOut Errors) const;
bool SetJson(const FJsonValue &Value, WingOut Errors) const;
// Fetch a value. If an error occurs such as a type
// mismatch, returns an empty optional and prints an
@@ -86,6 +85,7 @@ struct FWingProperty
// If mutable is false, all properties will be marked non-editable.
//
static TArray<FWingProperty> GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
static bool PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors);
// Convenience versions of GetAll and GetVisible for UObjects.
//
@@ -121,16 +121,6 @@ struct FWingProperty
//
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
// Functions to populate properties from a JSON object.
//
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json,
bool AllOptional, WingOut Errors);
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json,
bool AllOptional, WingOut Errors);
// Functions to populate properties from a JSON object.
//
private:
static bool IsUnsigned(FNumericProperty* Prop);
static bool IsPinTypeProperty(FProperty *Prop);

View File

@@ -62,9 +62,6 @@ public:
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
/** Package a list of response texts into a single serialized JSON content-block array. */
static FString PackageResponses(const TArray<FString>& Responses);
private:
static UWingServer* GWingServer;
@@ -79,10 +76,11 @@ private:
FDelegateHandle LoadingPhasesCompleteHandle;
FWingHandlerConfig* FindHandler(const FString& Name);
// Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line);
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
void TryCallHandler(TSharedPtr<FJsonObject> Request);
// Handle a complete request and return the response bytes.
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
void PreCallHandler();
FString PostCallHandler();
void TryCallHandler(TArrayView<const FString> Argv);
// ----- TCP server -----
FSocket* ListenSocket = nullptr;
@@ -99,22 +97,23 @@ private:
TArray<TSharedPtr<FClientConnection>> Clients;
void AcceptNewConnections();
void CleanupFinishedClients();
static uint32 UnpackBigEndian(const uint8 *Data);
static bool DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv);
static void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
static bool ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest);
static bool ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen);
static bool ReceiveRequest(
FSocket* Socket, TArray<uint8>& OutRequest);
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
static bool ProcessRequestOnGameThread(
const FString& Request, FString& Response);
const TArray<uint8>& Request, TArray<uint8>& Response);
static void WaitForAssetRegistry();
// ----- The Critical Section -----
struct FPendingMessage
{
FString Line;
TPromise<FString> Response;
FPendingMessage() : Response(TPromise<FString>()) {}
TArray<uint8> Request;
TPromise<TArray<uint8>> Response;
FPendingMessage() : Response(TPromise<TArray<uint8>>()) {}
};
FCriticalSection Mutex;
TArray<TSharedPtr<FPendingMessage>> PendingMessages;

View File

@@ -57,6 +57,9 @@ public:
// Empty the variable list.
void Empty() { Variables.Empty(); }
// Add a variable.
void Add(const Var &Var) { Variables.Add(Var); }
// Return true if the variables are empty.
bool IsEmpty() { return Variables.IsEmpty(); }
@@ -72,21 +75,11 @@ public:
// Check the sanity of the vars in the array. If allow
// is false, then no variables are allowed in the array.
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
// Parse variables from a string.
bool ParseString(const FString &Input, WingOut Errors);
// Parse variable names only from a string.
bool ParseNamesString(const FString &Input, WingOut Errors);
private:
bool ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
};
class WingVariables
{
public:
public:
using Var = WingVariableList::Var;
WingVariables() {}
@@ -125,6 +118,10 @@ public:
void Print(WingOut Out);
// Parse variables.
bool Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors);
// Load: clear the workspace, then
// copy everything from the backing store into the workspace.
@@ -193,4 +190,8 @@ private:
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
bool ErrorNoBackingStore(WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors);
WingVariableList *GetList(FName Name);
};

View File

@@ -1,178 +0,0 @@
#!/usr/bin/env python3
"""
MCP stdio-to-TCP bridge for UE Wingman.
Exposes a single MCP tool "unreal" that forwards JSON commands to the
UE Wingman TCP server in the Unreal Editor.
"""
import sys
import json
import socket
HOST = "localhost"
PORT = 9851
CONNECT_TIMEOUT = 2
READ_TIMEOUT = 30
TOOL_DESCRIPTION = (
"Send a command to the Unreal Editor's UE Wingman plugin. "
"The 'command' field specifies which operation to perform; "
"additional fields are command-specific parameters. "
'Use {"command": "Documentation_Manual"} to get an overview. '
"If the editor is not running, the call will return an error; "
"just ask the user to start the editor and try again."
)
TOOL_SCHEMA = {
"name": "unreal",
"description": TOOL_DESCRIPTION,
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "The command to execute"},
},
"required": ["command"],
"additionalProperties": True,
},
}
sock = None
def connect():
"""Try to connect to the editor. Returns True on success."""
global sock
if sock is not None:
return True
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(CONNECT_TIMEOUT)
s.connect((HOST, PORT))
s.settimeout(READ_TIMEOUT)
sock = s
return True
except (ConnectionRefusedError, socket.timeout, OSError):
return False
def disconnect():
global sock
if sock is not None:
try:
sock.close()
except Exception:
pass
sock = None
def send_and_receive(message):
"""Send a JSON message to the editor and return the null-terminated response."""
data = json.dumps(message) + "\0"
sock.sendall(data.encode())
result = b""
while True:
chunk = sock.recv(65536)
if not chunk:
raise ConnectionError("Connection closed")
result += chunk
if b"\0" in result:
break
return result[:result.index(b"\0")].decode()
def forward_to_editor(arguments):
"""Forward arguments to the editor, return the result dict."""
if not connect():
return {"error": "Unreal Editor is not running. Start the editor and try again."}
try:
return send_and_receive(arguments)
except Exception:
disconnect()
return {"error": "Lost connection to Unreal Editor."}
def make_jsonrpc(msg_id, result):
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
def parse_editor_response(result):
"""Parse and validate a raw editor response into an MCP content list.
MCP expects `content` to be a list of objects, each with at least a
string "type" field (e.g. {"type": "text", "text": "..."}). Anything
else is replaced with a single error item so the client sees a clear
message instead of a schema violation.
"""
try:
parsed = json.loads(result)
except json.JSONDecodeError:
return [{"type": "text", "text": "Malformed response from editor: invalid JSON."}]
if not isinstance(parsed, list):
return [{"type": "text", "text": "Malformed response from editor: expected a list."}]
for item in parsed:
if not isinstance(item, dict):
return [{"type": "text", "text": "Malformed response from editor: list item is not an object."}]
if not isinstance(item.get("type"), str):
return [{"type": "text", "text": "Malformed response from editor: item missing string 'type' field."}]
return parsed
def handle_message(msg):
"""Handle one JSON-RPC message from Claude Code."""
msg_id = msg.get("id")
method = msg.get("method", "")
# Notifications don't get responses
if msg_id is None:
return None
if method == "initialize":
return make_jsonrpc(msg_id, {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
})
if method == "tools/list":
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
if method == "tools/call":
params = msg.get("params", {})
arguments = params.get("arguments", {})
result = forward_to_editor(arguments)
if isinstance(result, dict) and "error" in result:
content = [{"type": "text", "text": result["error"]}]
else:
content = parse_editor_response(result)
return make_jsonrpc(msg_id, {
"content": content,
})
return {
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32601, "message": f"Method not found: {method}"},
}
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
response = handle_message(msg)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()

View File

@@ -1,37 +1,23 @@
#!/usr/bin/env python3
"""
Human-friendly MCP test client.
UE Wingman command-line tool. This tool simply packages up its
argv and sends it to the plugin, and then prints whatever the
plugin sends back. All the real work is done in the plugin.
Usage: ue-wingman.py <command> [key=value ...]
Values starting with '[' or '{' are parsed as JSON.
Usage: ue-wingman.py <arg1> [arg2 ...]
"""
import sys
import json
import socket
import struct
HOST = "localhost"
PORT = 9851
TIMEOUT = 120
TIMEOUT = 15
def main():
args = sys.argv[1:]
if not args:
print("Usage: ue-wingman.py <command> [key=value ...]")
sys.exit(1)
msg = {"command": args[0]}
for arg in args[1:]:
key, _, value = arg.partition("=")
if value and value[0] in ('[', '{'):
try:
value = json.loads(value)
except json.JSONDecodeError as e:
print(f"Bad JSON in {key}: {e.msg}")
sys.exit(1)
msg[key] = value
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT)
@@ -41,7 +27,14 @@ def main():
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
sys.exit(1)
sock.sendall((json.dumps(msg) + "\0").encode())
payload = bytearray()
for arg in args:
data = arg.encode()
payload += struct.pack("!I", len(data))
payload += data
sock.sendall(struct.pack("!I", len(payload)))
sock.sendall(payload)
sock.shutdown(socket.SHUT_WR)
result = b""
while True:
@@ -49,29 +42,9 @@ def main():
if not chunk:
break
result += chunk
if b"\0" in result:
break
sock.close()
result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode()
try:
parsed = json.loads(result)
except json.JSONDecodeError:
print("Error: response is not valid JSON.")
sys.exit(1)
if not isinstance(parsed, list):
print("Error: response is not a list of content blocks.")
sys.exit(1)
for block in parsed:
if not (isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)):
print("Error: response contains a non-text block.")
sys.exit(1)
print("\n---\n".join(block["text"] for block in parsed))
print(result.decode(), end="")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,6 @@
#pragma once
#include <string_view>
#include "CoreMinimal.h"
#include "CoreUObject.h"
#include "Containers/Deque.h"

View File

@@ -317,6 +317,12 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co
return Result;
}
FText UlxFormatDataLibrary::FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
FText InPatternText(FText::FromString(InPattern));
return FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
}
void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
// For throttled verbosity levels, suppress repeated messages with the
@@ -338,8 +344,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
// Generate the formatted string.
//
FText InPatternText(FText::FromString(InPattern));
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
FText Message = FormatMessageInternal(InPattern, MoveTemp(InArgs));
FString MessageString = Message.ToString();
// Get the blueprint name.

View File

@@ -105,9 +105,16 @@ public:
//
static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild);
// Format a message using FTextFormatter::Format.
// Meant to be used internally by the Format Message K2Node,
// which needs an impure wrapper around formatting.
//
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static FText FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs);
// Format a message using FTextFormatter::Format, and send
// it to UE_LOG. The Context object's name is used as the
// log category. Meant to be used internally by the Format
// log category. Meant to be used internally by the Format
// Log Message K2Node.
//
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))

View File

@@ -250,7 +250,7 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
}
else
{
FormatFunction = UKismetTextLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UKismetTextLibrary, Format));
FormatFunction = UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatMessageInternal));
}
// This is the node that does all the Format work and outputs the message.

View File

@@ -0,0 +1,105 @@
#include "InputDeviceTracker.h"
#include "Framework/Application/SlateApplication.h"
#include "GenericPlatform/GenericApplicationMessageHandler.h"
#include "InputCoreTypes.h"
// Last observed device classification. Read with no
// synchronization; updates happen on the game thread
// from Slate's input pipeline, and stale reads on
// other threads are acceptable for this use case.
//
static ElxControllerType GLastControllerType = ElxControllerType::KeyboardMouse;
// Keywords identifying a PlayStation-family gamepad.
// Matched case-insensitively against the InputDeviceName
// and HardwareDeviceIdentifier fields of the current
// FInputDeviceScope.
//
static const TCHAR* const PlaystationKeywords[] = {
TEXT("Playstation"),
TEXT("PS3"),
TEXT("PS4"),
TEXT("PS5"),
TEXT("PS6"),
TEXT("PS7"),
TEXT("Dualsense"),
TEXT("Dualshock"),
};
namespace
{
bool ContainsAnyPlaystationKeyword(const FString& Haystack)
{
for (const TCHAR* Keyword : PlaystationKeywords)
{
if (Haystack.Contains(Keyword, ESearchCase::IgnoreCase))
{
return true;
}
}
return false;
}
// Classifies the active gamepad by scanning the current
// FInputDeviceScope. Defaults to Xbox; switches to
// PlayStation only on a keyword match.
//
ElxControllerType ClassifyGamepadFromScope()
{
const FInputDeviceScope* Scope = FInputDeviceScope::GetCurrent();
if (Scope != nullptr)
{
if (ContainsAnyPlaystationKeyword(Scope->InputDeviceName.ToString()) ||
ContainsAnyPlaystationKeyword(Scope->HardwareDeviceIdentifier))
{
return ElxControllerType::PlayStationGamepad;
}
}
return ElxControllerType::XboxGamepad;
}
}
bool FInputDeviceTrackerProcessor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent)
{
if (KeyEvent.GetKey().IsGamepadKey())
{
GLastControllerType = ClassifyGamepadFromScope();
}
else
{
GLastControllerType = ElxControllerType::KeyboardMouse;
}
return false;
}
bool FInputDeviceTrackerProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
GLastControllerType = ElxControllerType::KeyboardMouse;
return false;
}
void UlxInputDeviceTracker::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (FSlateApplication::IsInitialized())
{
Processor = MakeShared<FInputDeviceTrackerProcessor>();
FSlateApplication::Get().RegisterInputPreProcessor(Processor);
}
}
void UlxInputDeviceTracker::Deinitialize()
{
if (Processor.IsValid() && FSlateApplication::IsInitialized())
{
FSlateApplication::Get().UnregisterInputPreProcessor(Processor);
}
Processor.Reset();
Super::Deinitialize();
}
ElxControllerType UlxInputDeviceTracker::GetLastControllerType()
{
return GLastControllerType;
}

View File

@@ -0,0 +1,64 @@
////////////////////////////////////////////////////////////
//
// InputDeviceTracker.h
//
// Tracks the most recently used input device, classifying
// it as keyboard/mouse, Xbox gamepad, or PlayStation
// gamepad. The subsystem registers a Slate input
// preprocessor that watches button-down events; analog
// and mouse-move events are ignored. Read the current
// classification via the static accessor.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Framework/Application/IInputProcessor.h"
#include "Common.h"
#include "InputDeviceTracker.generated.h"
////////////////////////////////////////////////////////////
//
// FInputDeviceTrackerProcessor
//
// Slate input preprocessor. Updates the device-class
// static on each button-down event. Never consumes
// events (always returns false).
//
////////////////////////////////////////////////////////////
class FInputDeviceTrackerProcessor : public IInputProcessor
{
public:
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent) override;
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
};
////////////////////////////////////////////////////////////
//
// UlxInputDeviceTracker
//
////////////////////////////////////////////////////////////
UCLASS(MinimalAPI)
class UlxInputDeviceTracker : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Returns the classification of the most recently used
// input device. Defaults to KeyboardMouse until the
// first gamepad button event is observed.
//
UFUNCTION(BlueprintCallable, Category="Luprex|Input")
static ElxControllerType GetLastControllerType();
private:
TSharedPtr<FInputDeviceTrackerProcessor> Processor;
};

View File

@@ -31,6 +31,7 @@ public class Integration : ModuleRules
"UMG",
"UMGEditor",
"EditorSubsystem",
"ApplicationCore",
});
PrivateDependencyModuleNames.Add("Slate");

View File

@@ -73,7 +73,6 @@ void ALuprexGameModeBase::UpdateConsoleOutput() {
}
}
#pragma optimize("", off)
void ALuprexGameModeBase::UpdateTangibles() {
double radius = 1000.0; // Hardwired for now.
using TanArray = UlxTangibleManager::TanArray;
@@ -132,8 +131,9 @@ void ALuprexGameModeBase::UpdatePossessedTangible() {
void ALuprexGameModeBase::UpdateLuaSourceCode() {
FlxLockedWrapper lockedwrap;
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()))
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()) || ReloadSource)
{
ReloadSource = false;
drvutil::ostringstream srcpak;
FString LuprexRoot = FPaths::Combine(FPaths::ProjectDir(), TEXT("luprex"));
std::string srcpakerr = drvutil::package_lua_source(TCHAR_TO_UTF8(*LuprexRoot), &srcpak);
@@ -259,4 +259,9 @@ ALuprexGameModeBase *ALuprexGameModeBase::FromContext(const UObject *context) {
return result;
}
void ALuprexGameModeBase::TriggerReloadSource(const UObject *WorldContextObject) {
ALuprexGameModeBase *GameMode = FromContext(WorldContextObject);
GameMode->ReloadSource = true;
}

View File

@@ -62,6 +62,11 @@ public:
// Get the current Luprex Game Mode Base, given a Context object.
static ALuprexGameModeBase *FromContext(const UObject *Context);
// Set the ReloadSource flag on the current Luprex game mode, causing
// the Lua source to be reloaded on the next tick.
UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject"))
static void TriggerReloadSource(const UObject *WorldContextObject);
// The sensitivity level at which a log message triggers a debugger breakpoint.
UPROPERTY(EditAnywhere, Category="Debugging Tools")
@@ -76,6 +81,9 @@ public:
// This is always true unless you use the debugger to set it to false.
bool TickEnabled = true;
// True to trigger a source reload.
bool ReloadSource = false;
// Current Player ID
int64 PlayerId = 0;

View File

@@ -1,8 +1,7 @@
#include "PromptWidget.h"
#include "InputDeviceTracker.h"
#include "UtilityLibrary.h"
#include "PlayerControllerBase.h"
#include "Engine/Engine.h"
#include "GameFramework/InputDeviceSubsystem.h"
#include "InputAction.h"
#include "InputMappingContext.h"
#include "Widgets/SOverlay.h"
@@ -22,6 +21,7 @@
// Row 7: Space Bar, unused, unused, unused
// Row 8: unused, unused, unused, unused.
int32 UlxPromptWidget::ChooseIcon(bool Playstation, FKey Key) const
{
// Mouse buttons (row 1)
@@ -68,15 +68,6 @@ void UlxPromptWidget::ChooseAppearance(int32 &OutIcon, FString &OutGlyph)
else OutGlyph = TEXT("");
}
void UlxPromptWidget::OnHardwareDeviceChanged(const FPlatformUserId UserId, const FInputDeviceId DeviceId)
{
ElxControllerType Type = UlxUtilityLibrary::DetectControllerType(GetOwningLocalPlayer());
if (ControllerType != Type)
{
ControllerType = Type;
SynchronizeProperties();
}
}
FBox2f UlxPromptWidget::GetIconUVs(int32 IconIndex)
{
@@ -202,15 +193,23 @@ void UlxPromptWidget::SetKeysFromBindings(const UInputMappingContext* InputMappi
SetKeys(NewGamepadKey, NewKeyboardKey);
}
bool UlxPromptWidget::OnTick(float DeltaTime)
{
ElxControllerType Type = UlxInputDeviceTracker::GetLastControllerType();
if (ControllerType != Type)
{
ControllerType = Type;
SynchronizeProperties();
}
return true;
}
TSharedRef<SWidget> UlxPromptWidget::RebuildWidget()
{
if (!IsDesignTime())
{
ControllerType = UlxUtilityLibrary::DetectControllerType(GetOwningLocalPlayer());
if (UInputDeviceSubsystem* IDS = GEngine->GetEngineSubsystem<UInputDeviceSubsystem>())
{
IDS->OnInputHardwareDeviceChanged.AddDynamic(this, &UlxPromptWidget::OnHardwareDeviceChanged);
}
ControllerType = UlxInputDeviceTracker::GetLastControllerType();
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UlxPromptWidget::OnTick));
}
MyBrush.SetResourceObject(ButtonAtlas.Get());
@@ -240,10 +239,7 @@ void UlxPromptWidget::ReleaseSlateResources(bool bReleaseChildren)
Super::ReleaseSlateResources(bReleaseChildren);
if (!IsDesignTime())
{
if (UInputDeviceSubsystem* IDS = GEngine->GetEngineSubsystem<UInputDeviceSubsystem>())
{
IDS->OnInputHardwareDeviceChanged.RemoveDynamic(this, &UlxPromptWidget::OnHardwareDeviceChanged);
}
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
}
MyBox.Reset();
MyOverlay.Reset();

View File

@@ -7,6 +7,7 @@
#include "Widgets/SOverlay.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Containers/Ticker.h"
#include "PromptWidget.generated.h"
class UInputAction;
@@ -62,8 +63,6 @@ public:
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetDepressed(bool InDepressed);
UFUNCTION()
void OnHardwareDeviceChanged(const FPlatformUserId UserId, const FInputDeviceId DeviceId);
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
@@ -84,4 +83,7 @@ private:
FMargin GetScaledMargins() const;
int32 ChooseIcon(bool Playstation, FKey Key) const;
void ChooseAppearance(int32 &OutIcon, FString &OutGlyph);
bool OnTick(float DeltaTime);
FTSTicker::FDelegateHandle TickHandle;
};

View File

@@ -0,0 +1,398 @@
#include "RadialMenu.h"
#include "Rendering/DrawElements.h"
#include "Engine/Texture2D.h"
#include "Fonts/FontMeasure.h"
#include "Framework/Application/SlateApplication.h"
#include "Styling/SlateBrush.h"
#include "Widgets/SLeafWidget.h"
TArray<FRadialMenuItem> FRadialMenuItem::Calculate(const FRadialMenuConfig &Config)
{
TArray<FRadialMenuItem> Items;
const int32 NumItems = Config.MenuItems.Num();
if (NumItems <= 0) return Items;
int32 NumRight = (NumItems / 2);
int32 NumLeft = NumItems - NumRight;
Items.SetNum(NumItems);
// Measure each non-empty label first; the spoke layout's item height
// is derived from the tallest label.
const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
double MaxTextHeight = 5.0;
for (int32 I = 0; I < NumItems; I++)
{
if (Config.MenuItems[I].IsEmpty()) continue;
Items[I].TextSize = FontMeasure->Measure(Config.MenuItems[I], Config.Font);
MaxTextHeight = FMath::Max(MaxTextHeight, Items[I].TextSize.Y);
}
const float ItemHeight = static_cast<float>(MaxTextHeight * 1.2);
View LeftItems(Items.GetData(), NumLeft);
View RightItems(Items.GetData() + NumLeft, NumRight);
CalculateSpokes(LeftItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
CalculateSpokes(RightItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
double LeftWidth = WidestSpoke(LeftItems);
double RightWidth = WidestSpoke(RightItems);
double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread;
CalculateSpread(LeftItems, HalfWidth - LeftWidth);
CalculateSpread(RightItems, HalfWidth - RightWidth);
FlipHorizontal(LeftItems);
return Items;
}
TArray<FVector2D> FRadialMenuItem::CalculateDirections(int32 NumItems)
{
TArray<FVector2D> Result;
if (NumItems <= 0) return Result;
const int32 NumRight = (NumItems / 2);
const int32 NumLeft = NumItems - NumRight;
Result.SetNum(NumItems);
// Left side: SpokeVector with X flipped (matches FlipHorizontal in Calculate).
for (int32 I = 0; I < NumLeft; I++)
{
FVector2D V = SpokeVector(I, NumLeft, NumItems);
V.X = -V.X;
Result[I] = V;
}
for (int32 I = 0; I < NumRight; I++)
{
Result[NumLeft + I] = SpokeVector(I, NumRight, NumItems);
}
return Result;
}
FVector2D FRadialMenuItem::SpokeVector(int32 I, int32 NSide, int32 NTotal)
{
double SpokeAngle = 1.0 / NTotal;
double OffsetAngle = 0.5 * (0.5 - ((NSide - 1) * SpokeAngle));
double Revolutions = (I * SpokeAngle) + OffsetAngle;
double Radians = (Revolutions * 2.0 * UE_PI);
return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians));
}
void FRadialMenuItem::FlipHorizontal(View V)
{
for (FRadialMenuItem& Item : V)
{
Item.Point1.X = -Item.Point1.X;
Item.Point2.X = -Item.Point2.X;
Item.Point3.X = -Item.Point3.X;
Item.RightSide = !Item.RightSide;
}
}
double FRadialMenuItem::WidestSpoke(View V)
{
double Result = 0.0;
for (const FRadialMenuItem &Item : V)
{
Result = FMath::Max(Item.Point2.X, Result);
}
return Result;
}
void FRadialMenuItem::CalculateSpread(View V, double Offset)
{
for (FRadialMenuItem &Item : V)
{
Item.Point3 = Item.Point2 + FVector2D(Offset, 0.0);
}
}
void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke)
{
if (V.Num() == 0) return;
// RightSide is always initialized to true, it may get
// reversed by FlipHorizontal.
for (int32 I = 0; I < V.Num(); I++)
{
V[I].RightSide = true;
V[I].Point1 = SpokeVector(I, V.Num(), TotalItems) * InnerRadius;
}
// Calculate point2 for all spokes.
double NextLineMin = ItemHeight * 0.5;
int32 Mid = (V.Num() / 2);
if (V.Num() & 1)
{
V[Mid].Point2 = FVector2D(InnerRadius + MinSpoke, 0.0);
NextLineMin = ItemHeight;
Mid += 1;
}
for (int32 I = Mid; I < V.Num(); I++)
{
FVector2D UnitVec = SpokeVector(I, V.Num(), TotalItems);
double Y = (UnitVec.Y * (InnerRadius + MinSpoke));
if (Y < NextLineMin) Y = NextLineMin;
NextLineMin = Y + ItemHeight;
FVector2D Point2 = UnitVec * (Y / UnitVec.Y);
V[I].Point2 = Point2;
V[V.Num() - 1 - I].Point2 = Point2 * FVector2D(1.0,-1.0);
}
// The middle spoke is calculated using a different formula,
// which may result in a short spoke. If so, fix it to make
// it at least as long as the adjacent spoke.
if ((V.Num() & 1) && (V.Num() >= 3))
{
Mid = V.Num() / 2;
if (V[Mid].Point2.X < V[Mid + 1].Point2.X)
V[Mid].Point2.X = V[Mid + 1].Point2.X;
}
}
// Slate widget that paints the radial menu's polylines.
class SRadialMenu : public SLeafWidget
{
public:
SLATE_BEGIN_ARGS(SRadialMenu) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, const FRadialMenuConfig& InConfig)
{
Config = InConfig;
}
void SetConfig(const FRadialMenuConfig& NewConfig)
{
Config = NewConfig;
Items = FRadialMenuItem::Calculate(Config);
Invalidate(EInvalidateWidgetReason::Layout);
}
void SetPointer(FVector2D NewPointer)
{
if (PointerVector == NewPointer) return;
PointerVector = NewPointer;
Invalidate(EInvalidateWidgetReason::Paint);
}
protected:
virtual FVector2D ComputeDesiredSize(float) const override
{
FVector2D Min(0.0, 0.0);
FVector2D Max(0.0, 0.0);
for (const FRadialMenuItem &Item : Items)
{
for (const FVector2D &P : { Item.Point1, Item.Point2, Item.Point3 })
{
Min.X = FMath::Min(Min.X, P.X);
Min.Y = FMath::Min(Min.Y, P.Y);
Max.X = FMath::Max(Max.X, P.X);
Max.Y = FMath::Max(Max.Y, P.Y);
}
if (Item.TextSize.X <= 0) continue;
const double Gap = Config.Spread * 0.5;
const double TextLeft = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
const double TextRight = Item.RightSide ? (Item.Point3.X + Gap + Item.TextSize.X) : (Item.Point3.X - Gap);
Min.X = FMath::Min(Min.X, TextLeft);
Max.X = FMath::Max(Max.X, TextRight);
Min.Y = FMath::Min(Min.Y, Item.Point3.Y - Item.TextSize.Y * 0.5);
Max.Y = FMath::Max(Max.Y, Item.Point3.Y + Item.TextSize.Y * 0.5);
}
// Symmetric around the origin so the menu draws centered.
double HalfW = FMath::Max(FMath::Abs(Min.X), FMath::Abs(Max.X));
double HalfH = FMath::Max(FMath::Abs(Min.Y), FMath::Abs(Max.Y));
return FVector2D(HalfW * 2.0, HalfH * 2.0);
}
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
{
const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5;
const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry();
for (int32 I = 0; I < Items.Num(); I++)
{
const FRadialMenuItem& Item = Items[I];
const bool bSelected = (I == Config.SelectedItem);
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
const float Thickness = bSelected ? Config.SelectedThickness : Config.LineThickness;
TArray<FVector2D> Points;
Points.Add(Center + Item.Point1);
Points.Add(Center + Item.Point2);
Points.Add(Center + Item.Point3);
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, Thickness);
}
// Draw item labels, vertically centered on Point3. Right-side items
// are left-aligned to Point3; left-side items are right-aligned.
for (int32 I = 0; I < Items.Num(); I++)
{
const FRadialMenuItem& Item = Items[I];
if (Item.TextSize.X <= 0) continue;
const bool bSelected = (I == Config.SelectedItem);
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
const double Gap = Config.Spread * 0.5;
FVector2D TextPos;
TextPos.X = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
TextPos.Y = Item.Point3.Y - Item.TextSize.Y * 0.5;
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 1,
AllottedGeometry.ToPaintGeometry(Item.TextSize, FSlateLayoutTransform(Center + TextPos)),
Config.MenuItems[I],
Config.Font,
ESlateDrawEffect::None,
Color);
}
if (Config.DotRadius > 0.0f && Config.DotTexture != nullptr)
{
FSlateBrush DotBrush;
DotBrush.SetResourceObject(Config.DotTexture);
const FVector2D DotSize(Config.DotRadius * 2.0f);
for (int32 I = 0; I < Items.Num(); I++)
{
const FRadialMenuItem& Item = Items[I];
const bool bSelected = (I == Config.SelectedItem);
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
for (const FVector2D& Point : { Item.Point1, Item.Point3 })
{
const FVector2D LocalPoint = Center + Point;
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 1,
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))),
&DotBrush,
ESlateDrawEffect::None,
Color);
}
}
}
// Teardrop: visualizes PointerVector inside InnerRadius. The texture's
// tip naturally points up (0,-1); rotate it to align with PointerVector.
// When PointerVector is (0,0), the teardrop stays upright at the center.
if (Config.TeardropRadius > 0.0f && Config.Teardrop != nullptr)
{
FSlateBrush TeardropBrush;
TeardropBrush.SetResourceObject(Config.Teardrop);
const FVector2D TeardropSize(Config.TeardropRadius * 2.0f);
const FVector2D TeardropCenter = Center + PointerVector * Config.InnerRadius;
const float Angle = PointerVector.IsZero()
? 0.0f
: static_cast<float>(FMath::Atan2(PointerVector.X, -PointerVector.Y));
FSlateDrawElement::MakeRotatedBox(
OutDrawElements,
LayerId + 1,
AllottedGeometry.ToPaintGeometry(TeardropSize, FSlateLayoutTransform(TeardropCenter - FVector2D(Config.TeardropRadius))),
&TeardropBrush,
ESlateDrawEffect::None,
Angle);
}
return LayerId + 1;
}
private:
FRadialMenuConfig Config;
TArray<FRadialMenuItem> Items;
FVector2D PointerVector = {0,0};
};
void URadialMenuWidget::SetMenuItems(const TArray<FString>& NewMenuItems)
{
if (Config.MenuItems == NewMenuItems) return;
Config.MenuItems = NewMenuItems;
Config.SelectedItem = -1;
PointerVector = FVector2D();
SynchronizeProperties();
}
FString URadialMenuWidget::GetSelectedMenuItem() const
{
if (!Config.MenuItems.IsValidIndex(Config.SelectedItem)) return FString();
return Config.MenuItems[Config.SelectedItem];
}
void URadialMenuWidget::SetSelectedItem(int32 NewSelectedItem)
{
if (Config.SelectedItem == NewSelectedItem) return;
Config.SelectedItem = NewSelectedItem;
SynchronizeProperties();
}
void URadialMenuWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
if (Directions.Num() != Config.MenuItems.Num())
{
Directions = FRadialMenuItem::CalculateDirections(Config.MenuItems.Num());
}
if (MySlateWidget.IsValid())
{
MySlateWidget->SetConfig(Config);
MySlateWidget->SetPointer(PointerVector);
}
}
void URadialMenuWidget::AddPointer(FVector2D Direction, float Scale)
{
SetPointer(PointerVector + (Direction * Scale), 1.0);
}
void URadialMenuWidget::SetPointer(FVector2D Direction, float Scale)
{
if ((!IsVisible()) || (Directions.Num() == 0))
{
PointerVector = FVector2D();
SetSelectedItem(-1);
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
return;
}
PointerVector = (Direction * Scale);
if (PointerVector.Length() < 0.75)
{
SetSelectedItem(-1);
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
return;
}
if (PointerVector.Length() > 1.0)
{
PointerVector.Normalize();
}
int32 BestIndex = -1;
double BestDot = -2.0;
for (int32 I = 0; I < Directions.Num(); I++)
{
FVector2D SpokeDir = Directions[I];
if (!SpokeDir.Normalize()) continue;
double Dot = FVector2D::DotProduct(SpokeDir, PointerVector);
if (Dot <= BestDot) continue;
BestDot = Dot;
BestIndex = I;
}
SetSelectedItem(BestIndex);
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
}
TSharedRef<SWidget> URadialMenuWidget::RebuildWidget()
{
MySlateWidget = SNew(SRadialMenu, Config);
return MySlateWidget.ToSharedRef();
}
void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MySlateWidget.Reset();
}

View File

@@ -0,0 +1,164 @@
//
// This class implements the layout calculatations for a radial
// menu.
//
#pragma once
#include "CoreMinimal.h"
#include "Components/Widget.h"
#include "Fonts/SlateFontInfo.h"
#include "RadialMenu.generated.h"
class SRadialMenu;
class UTexture2D;
USTRUCT(BlueprintType)
struct FRadialMenuConfig
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category="RadialMenu")
TArray<FString> MenuItems;
UPROPERTY(EditAnywhere, Category="RadialMenu")
FSlateFontInfo Font;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float InnerRadius = 20.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float MinSpoke = 20.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float Spread = 20.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float LineThickness = 4.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
FLinearColor ItemColor = FLinearColor::White;
UPROPERTY(EditAnywhere, Category="RadialMenu")
TObjectPtr<UTexture2D> DotTexture;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float DotRadius = 3.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
TObjectPtr<UTexture2D> Teardrop;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float TeardropRadius = 0.0f;
UPROPERTY(EditAnywhere, Category="RadialMenu")
int32 SelectedItem = -1;
UPROPERTY(EditAnywhere, Category="RadialMenu")
FLinearColor SelectedColor = FLinearColor::Yellow;
UPROPERTY(EditAnywhere, Category="RadialMenu")
float SelectedThickness = 2.0f;
};
USTRUCT(BlueprintType)
struct FRadialMenuItem
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
FVector2D Point1 = {0,0};
UPROPERTY(BlueprintReadOnly)
FVector2D Point2 = {0,0};
UPROPERTY(BlueprintReadOnly)
FVector2D Point3 = {0,0};
UPROPERTY(BlueprintReadOnly)
bool RightSide = false;
UPROPERTY(BlueprintReadOnly)
FVector2D TextSize = {0,0};
static TArray<FRadialMenuItem> Calculate(const FRadialMenuConfig &Config);
// Like Calculate, but only produces the unit-vector direction of
// each spoke. Cheaper to evaluate when only the spoke directions
// are needed (e.g. for hit-testing a pointer against the wheel).
static TArray<FVector2D> CalculateDirections(int32 NumItems);
private:
using View = TArrayView<FRadialMenuItem>;
// Give the unit vector for the selected spoke. NSide is
// the number of spokes on the side of the wheel that
// we're calculating, and NTotal is the total number of
// spokes on both sides. The spokes on a given side are
// organized top-to-bottom, and the angle between the
// spokes is always equal to (1/NTotal) of the circle.
static FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal);
// Populate Point1 and Point2, these are the
// endpoints of the spoke segment. Spokes are
// designed to always be long enough to make room
// for MinSpoke, but also to make enough room to
// keep the menu items from overlapping.
static void CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke);
// Search for the widest spoke, and return its X coordinate.
static double WidestSpoke(View V);
// Populate Point3, this is the endpoint of the spread
// line that goes horizontal.
static void CalculateSpread(View V, double Offset);
// Flip everything in the specified view horizontally.
static void FlipHorizontal(View V);
};
UCLASS(BlueprintType, Blueprintable)
class URadialMenuWidget : public UWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void SetMenuItems(const TArray<FString>& NewMenuItems);
UFUNCTION(BlueprintCallable)
void ClearMenuItems() { SetMenuItems({}); }
UFUNCTION(BlueprintCallable)
void SetSelectedItem(int32 NewSelectedItem);
// Returns true if the selected item changed.
UFUNCTION(BlueprintCallable)
void AddPointer(FVector2D Direction, float Scale);
UFUNCTION(BlueprintCallable)
void SetPointer(FVector2D Direction, float Scale);
UFUNCTION(BlueprintCallable)
FVector2D GetPointer() const { return PointerVector; }
UFUNCTION(BlueprintCallable)
FString GetSelectedMenuItem() const;
protected:
UPROPERTY(EditAnywhere)
FRadialMenuConfig Config;
TArray<FVector2D> Directions;
FVector2D PointerVector = {0,0};
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
virtual void SynchronizeProperties() override;
TSharedPtr<SRadialMenu> MySlateWidget;
};

View File

@@ -0,0 +1,226 @@
#include "ReadSlashCommand.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_CallFunction.h"
#include "KismetCompiler.h"
#include "LuaCall.h"
#include "SlashCommand.h"
#define LOCTEXT_NAMESPACE "ReadSlashCommand"
const FName UK2Node_ReadSlashCommand::PrototypePinName(TEXT("Prototype"));
const FName UK2Node_ReadSlashCommand::InputValuesPinName(TEXT("Input Values"));
const FName UK2Node_ReadSlashCommand::ErrorPinName(TEXT("Error"));
bool UK2Node_ReadSlashCommand::ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps)
{
TArray<FString> Words;
Prototype.ParseIntoArrayWS(Words);
// The command name must be a slash followed by alphanumerics.
if (Words.Num() == 0 || !UlxSlashCommand::IsSlashCommand(Words[0]))
{
SetErrorMsg(TEXT("The prototype must start with a command name, e.g. \"/foo\"."));
Steps.Empty();
return false;
}
Steps.SetNum(Words.Num());
Steps[0].Word = Words[0];
TSet<FName> UsedNames;
for (int32 i = 1; i < Words.Num(); i++)
{
const FString& Word = Words[i];
FString FuncName = FString(TEXT("Read")) + Word;
UFunction* ReadFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(FName(*FuncName));
if (ReadFunc == nullptr)
{
SetErrorMsg(FString::Printf(TEXT("Unknown value type: %s"), *Word));
Steps.Empty();
return false;
}
FName PinName = AddPrefix(Word, 'R');
for (int Suffix = 2; UsedNames.Contains(PinName); Suffix++)
{
PinName = AddPrefix(FString::Printf(TEXT("%s%d"), *Word, Suffix), 'R');
}
Steps[i].Word = Word;
Steps[i].PinName = PinName;
Steps[i].ReadFunction = ReadFunc;
UsedNames.Add(PinName);
}
return true;
}
FText UK2Node_ReadSlashCommand::GetTooltipText() const
{
static FText Tooltip = FText::FromString(TEXT(
"Parse a slash command.\n"
"\n"
"The prototype must be a hardwired string. The first word\n"
"is the command name; each remaining word names a value to\n"
"read and becomes an output pin.\n"
"\n"
"For example:\n"
"\n"
" /command Integer Float\n"
"\n"
"Supported types: Token, Integer, Float, Rest\n"));
return Tooltip;
}
void UK2Node_ReadSlashCommand::ReconstructNode()
{
// Save the value of the Prototype Pin before it gets reconstructed.
UEdGraphPin* PrototypePin = FindPin(PrototypePinName);
if (PrototypePin != nullptr)
{
ValuePrototype = PrototypePin->DefaultValue;
}
Super::ReconstructNode();
}
void UK2Node_ReadSlashCommand::AllocateDefaultPins()
{
Pins.Reset();
Super::AllocateDefaultPins();
TArray<ParsingStep> Steps;
ParsePrototype(ValuePrototype, Steps);
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, ErrorPinName);
UEdGraphPin *PrototypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, PrototypePinName);
PrototypePin->DefaultValue = ValuePrototype;
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UlxSlashCommand::StaticClass(), InputValuesPinName);
// Create an output pin for each value word.
for (int32 i = 1; i < Steps.Num(); i++)
{
// The value comes back through the function's "Result" out-parameter.
CreatePin(EGPD_Output, PropertyToPinType(Steps[i].ReadFunction->FindPropertyByName(TEXT("Result"))), Steps[i].PinName);
}
}
FText UK2Node_ReadSlashCommand::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return LOCTEXT("ReadSlashCommand_Title", "Read Slash Command");
}
FText UK2Node_ReadSlashCommand::GetPinDisplayName(const UEdGraphPin* Pin) const
{
// These pins don't need labels.
if ((Pin->PinName == UEdGraphSchema_K2::PN_Execute) ||
(Pin->PinName == UEdGraphSchema_K2::PN_Then) ||
(Pin->PinName == PrototypePinName))
{
return FText::GetEmpty();
}
// Return the pin name, removing R: prefix if present.
return FText::FromName(RemovePrefix(Pin->PinName));
}
void UK2Node_ReadSlashCommand::PinDefaultValueChanged(UEdGraphPin* Pin)
{
if ((Pin->PinName == PrototypePinName) && (Pin->DefaultValue != ValuePrototype))
{
ReconstructNode();
}
}
void UK2Node_ReadSlashCommand::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
TArray<ParsingStep> Steps;
if (!ParsePrototype(ValuePrototype, Steps))
{
CompilerContext.MessageLog.Error(*ErrorMsg);
BreakAllNodeLinks();
return;
}
UEdGraphPin *InputSlashCommandPin = FindPinChecked(InputValuesPinName);
UEdGraphPin *ErrorExecPin = FindPinChecked(ErrorPinName);
UFunction *CheckCommandFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(TEXT("CheckCommand"));
UK2Node_CallFunction *CheckCommandNode = MakeCallFunctionNode(CompilerContext, SourceGraph, CheckCommandFunc);
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *CheckCommandNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *CheckCommandNode->GetExecPin());
CheckCommandNode->FindPinChecked(TEXT("Literal"))->DefaultValue = Steps[0].Word;
CheckCommandNode->FindPinChecked(TEXT("Prototype"))->DefaultValue = ValuePrototype;
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *CheckCommandNode->FindPinChecked(TEXT("Error")));
UEdGraphPin *ThenPin = CheckCommandNode->FindPinChecked(TEXT("Success"));
for (int32 i = 1; i < Steps.Num(); i++)
{
UK2Node_CallFunction *ReadNode = MakeCallFunctionNode(CompilerContext, SourceGraph, Steps[i].ReadFunction);
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *ReadNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *ReadNode->FindPinChecked(TEXT("Error")));
UEdGraphPin *OutputPin = FindPinChecked(Steps[i].PinName);
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *ReadNode->FindPinChecked(TEXT("Result")));
ThenPin = ChainExecPin(ThenPin, ReadNode, TEXT("Success"));
}
CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *ThenPin);
BreakAllNodeLinks();
}
UK2Node::ERedirectType UK2Node_ReadSlashCommand::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const
{
if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None;
if ((NewPin->PinName == OldPin->PinName) &&
(NewPin->Direction == OldPin->Direction) &&
(NewPin->PinType == OldPin->PinType))
{
return ERedirectType_Name;
}
return ERedirectType_None;
}
bool UK2Node_ReadSlashCommand::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const
{
// The prototype pin cannot be connected.
if (MyPin->PinName == PrototypePinName)
{
OutReason = LOCTEXT("Error_PrototypeMustBeHardwired", "Value prototype must be a hardwired constant.").ToString();
return true;
}
return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason);
}
void UK2Node_ReadSlashCommand::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
UClass* ActionKey = GetClass();
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
{
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(NodeSpawner != nullptr);
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
}
}
FText UK2Node_ReadSlashCommand::GetMenuCategory() const
{
return FText::FromString(FString(TEXT("Luprex|Slash Commands")));
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,74 @@
////////////////////////////////////////////////////////////
//
// ReadSlashCommand.h
//
// K2Node that reads typed values from a UlxLuaValues array.
// Takes a prototype string like "string x, float y, int z"
// and creates output pins with the appropriate types.
//
////////////////////////////////////////////////////////////
#pragma once
#include "LuprexK2Node.h"
#include "ReadSlashCommand.generated.h"
class FBlueprintActionDatabaseRegistrar;
class FString;
class UEdGraph;
class UObject;
UCLASS(MinimalAPI)
class UK2Node_ReadSlashCommand : public UlxK2Node
{
GENERATED_BODY()
public:
//~ Begin UEdGraphNode Interface.
virtual void AllocateDefaultPins() override;
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual bool ShouldShowNodeProperties() const override { return true; }
virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;
virtual FText GetTooltipText() const override;
virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override;
//~ End UEdGraphNode Interface.
//~ Begin UK2Node Interface.
virtual bool IsNodePure() const override { return false; }
virtual void ReconstructNode() override;
virtual bool NodeCausesStructuralBlueprintChange() const override { return true; }
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
virtual ERedirectType DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const override;
virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override;
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual FText GetMenuCategory() const override;
virtual int32 GetNodeRefreshPriority() const override { return EBaseNodeRefreshPriority::Low_UsesDependentWildcard; }
//~ End UK2Node Interface.
private:
static const FName PrototypePinName;
static const FName InputValuesPinName;
static const FName ErrorPinName;
struct ParsingStep
{
FString Word;
FName PinName;
UFunction *ReadFunction;
};
bool ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps);
private:
// Whenever the prototype pin value changes, we call
// ReconstructNode, which backs up the value into this
// property. This cache is needed because during
// ReconstructNode, we blow away the prototype pin. The
// prototype pin is also absent when the node is first
// created.
//
UPROPERTY()
FString ValuePrototype = TEXT("/command Integer Float");
};

View File

@@ -0,0 +1,157 @@
#include "SlashCommand.h"
#include "CoreMinimal.h"
#include "Containers/StringView.h"
#include "Misc/Char.h"
#include "Misc/DefaultValueHelper.h"
//////////////////////////////////////////////////////////////
//
// SlashCommand
//
// A command line plus a cursor, exposed to blueprint.
//
//////////////////////////////////////////////////////////////
UlxSlashCommand* UlxSlashCommand::MakeSlashCommand(const FString& CommandLine)
{
UlxSlashCommand* Result = NewObject<UlxSlashCommand>();
Result->CommandLine = CommandLine;
return Result;
}
bool UlxSlashCommand::IsSlashCommand(const FString& Command)
{
if (Command.Len() < 2 || Command[0] != TEXT('/'))
{
return false;
}
// Every character after the slash must be alphanumeric.
for (int32 i = 1; i < Command.Len(); i++)
{
if (!FChar::IsAlnum(Command[i]))
{
return false;
}
}
return true;
}
FStringView UlxSlashCommand::FetchToken()
{
const TCHAR* Data = *CommandLine;
int32 Len = CommandLine.Len();
// Skip leading whitespace.
while (Cursor < Len && FChar::IsWhitespace(Data[Cursor]))
{
Cursor++;
}
// Read characters up to the next whitespace.
int32 Start = Cursor;
while (Cursor < Len && !FChar::IsWhitespace(Data[Cursor]))
{
Cursor++;
}
return FStringView(Data + Start, Cursor - Start);
}
ElxSuccessOrError UlxSlashCommand::CheckCommand(const FString& Literal, const FString& Prototype)
{
KnownCommands.Add(Literal);
// Checking the command always starts a fresh parse.
Cursor = 0;
FStringView Token = FetchToken();
if (Token.Equals(Literal, ESearchCase::IgnoreCase))
{
MatchingPrototypes.Add(Prototype);
return ElxSuccessOrError::Success;
}
return ElxSuccessOrError::Error;
}
ElxSuccessOrError UlxSlashCommand::ReadToken(FString& Result)
{
FStringView Token = FetchToken();
if (Token.IsEmpty())
{
Result.Empty();
return ElxSuccessOrError::Error;
}
Result = FString(Token);
return ElxSuccessOrError::Success;
}
ElxSuccessOrError UlxSlashCommand::ReadInteger(int32& Result)
{
// ParseInt validates the whole token and converts it, so "12abc"
// is rejected rather than read as a partial number.
FStringView Token = FetchToken();
if (!FDefaultValueHelper::ParseInt(FString(Token), Result))
{
Result = 0;
return ElxSuccessOrError::Error;
}
return ElxSuccessOrError::Success;
}
ElxSuccessOrError UlxSlashCommand::ReadFloat(double& Result)
{
// ParseDouble validates the whole token and converts it, so
// "12abc" is rejected rather than read as a partial number.
FStringView Token = FetchToken();
if (!FDefaultValueHelper::ParseDouble(FString(Token), Result))
{
Result = 0.0;
return ElxSuccessOrError::Error;
}
return ElxSuccessOrError::Success;
}
ElxSuccessOrError UlxSlashCommand::ReadRest(FString& Result)
{
const TCHAR* Data = *CommandLine;
int32 Len = CommandLine.Len();
// Everything from the cursor to the end of the line, trimmed.
Result = FString(FStringView(Data + Cursor, Len - Cursor));
Result.TrimStartAndEndInline();
Cursor = Len;
return ElxSuccessOrError::Success;
}
FString UlxSlashCommand::GetErrorMessage() const
{
if (MatchingPrototypes.Num() == 0)
{
TArray<FString> Commands;
for (const FString& Command : KnownCommands)
{
Commands.Add(Command);
}
Commands.Sort();
return FString(TEXT("No such slash command. Valid slash commands: ")) + FString::Join(Commands, TEXT(", "));
}
FString Result = TEXT("Invalid parameters. Valid parameters:\n");
for (const FString& Prototype : MatchingPrototypes)
{
Result += Prototype;
Result += TEXT("\n");
}
return Result;
}

View File

@@ -0,0 +1,121 @@
////////////////////////////////////////////////////////////
//
// SlashCommand.h
//
// A command line plus a cursor, exposed to blueprint.
//
////////////////////////////////////////////////////////////
#pragma once
#include "Containers/Array.h"
#include "Containers/Set.h"
#include "Containers/UnrealString.h"
#include "Containers/StringFwd.h"
#include "Common.h"
#include "SlashCommand.generated.h"
////////////////////////////////////////////////////////////
//
// UlxSlashCommand
//
// Holds a command line (a string the user has typed) together
// with a cursor marking the current position within it. This
// is the object that wraps Unreal's FParse facilities so that
// blueprint can parse a typed command piece by piece, with the
// cursor advancing as each token is consumed.
//
////////////////////////////////////////////////////////////
UCLASS(BlueprintType)
class UlxSlashCommand : public UObject
{
GENERATED_BODY()
private:
// The full command line that we are parsing.
//
FString CommandLine;
// The current parse position: an index into CommandLine.
//
int32 Cursor = 0;
// Known command names that have been registered while exploring
// possible parses for this line.
//
TSet<FString> KnownCommands;
// Prototypes whose command name matches this line.
//
TArray<FString> MatchingPrototypes;
// Skip leading whitespace at the cursor, then read characters up
// to (but not including) the next whitespace. The token is
// returned as a view into CommandLine, and the cursor is advanced
// past it.
//
// Returns an empty view if there is no nonwhitespace input left.
//
FStringView FetchToken();
public:
// Construct a slash command from a command line string.
// The cursor starts at the beginning.
//
UFUNCTION(BlueprintCallable)
static UlxSlashCommand* MakeSlashCommand(const FString& CommandLine);
// Return true if the string is a slash followed by one or more
// alphanumeric characters, and nothing else (e.g. "/foo").
//
static bool IsSlashCommand(const FString& Command);
// Reset the cursor to the start, then read the first token and
// check whether it matches the given literal, case-insensitively.
// The command name is recorded in KnownCommands either way. If it
// matched, the prototype is added to MatchingPrototypes. The token
// is consumed either way. Returns Success if it matched, Error
// otherwise.
//
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
ElxSuccessOrError CheckCommand(const FString& Literal, const FString& Prototype);
// Read the next whitespace-delimited word from the command line.
//
// This is the blueprint-callable form of FetchToken: it returns
// the token as an FString. Returns Error (with an empty Result)
// if there is no nonwhitespace input left, Success otherwise.
//
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
ElxSuccessOrError ReadToken(FString& Result);
// Read the next token and interpret it as an integer, using C++
// number syntax (optional sign; decimal, 0x hex, or leading-0
// octal). The whole token must be valid; "12abc" is rejected.
// Returns Error (with Result 0) on failure, Success otherwise.
//
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
ElxSuccessOrError ReadInteger(int32& Result);
// Read the next token and interpret it as a floating-point number,
// using C++ number syntax (optional sign, digits, dot, e/E
// exponent, trailing f). The whole token must be valid; "12abc"
// is rejected. Returns Error (with Result 0) on failure, Success
// otherwise.
//
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
ElxSuccessOrError ReadFloat(double& Result);
// Read the rest of the command line, from the cursor to the end,
// trimmed of leading and trailing whitespace. The cursor is
// advanced to the end. Always returns Success.
//
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
ElxSuccessOrError ReadRest(FString& Result);
// Return a user-facing error message describing why parsing failed.
//
UFUNCTION(BlueprintCallable)
FString GetErrorMessage() const;
};

View File

@@ -10,11 +10,12 @@
#include "Kismet/GameplayStatics.h"
#include "Blueprint/UserWidget.h"
#include "Components/GridPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "Components/Widget.h"
#include "InputMappingContext.h"
#include "EnhancedInputComponent.h"
#include "Animation/AnimSequenceBase.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/InputDeviceSubsystem.h"
#include "GameFramework/InputSettings.h"
@@ -157,65 +158,28 @@ bool UlxUtilityLibrary::LineTraceThroughPixel(const APlayerController* PlayerCon
return false;
}
void UlxUtilityLibrary::SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY)
void UlxUtilityLibrary::ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent)
{
if ((GridPanel == nullptr) || (GridPanel->ColumnFill.Num() != 3) || (GridPanel->RowFill.Num() != 3))
UCanvasPanelSlot *CanvasSlot = Cast<UCanvasPanelSlot>(Target);
if (CanvasSlot == nullptr)
{
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
UWidget *Widget = Cast<UWidget>(Target);
if (Widget != nullptr)
{
CanvasSlot = Cast<UCanvasPanelSlot>(Widget->Slot);
}
}
if (CanvasSlot == nullptr)
{
UE_LOG(LogBlueprint, Error, TEXT("ConfigureCanvasPanelSlot: object is not a CanvasPanelSlot, and is not a Widget in a CanvasPanel."));
return;
}
if ((LowerRightXY.X < UpperLeftXY.X) || (LowerRightXY.Y < UpperLeftXY.Y))
{
UE_LOG(LogBlueprint, Error, TEXT("LowerRightXY must be greater than or equal to UpperLeftXY"));
return;
}
UpperLeftXY.X = FMath::Clamp(UpperLeftXY.X, 0.0f, 1.0f);
UpperLeftXY.Y = FMath::Clamp(UpperLeftXY.Y, 0.0f, 1.0f);
LowerRightXY.X = FMath::Clamp(LowerRightXY.X, 0.0f, 1.0f);
LowerRightXY.Y = FMath::Clamp(LowerRightXY.Y, 0.0f, 1.0f);
GridPanel->SetRowFill(0, UpperLeftXY.Y);
GridPanel->SetRowFill(1, LowerRightXY.Y - UpperLeftXY.Y);
GridPanel->SetRowFill(2, 1.0 - LowerRightXY.Y);
GridPanel->SetColumnFill(0, UpperLeftXY.X);
GridPanel->SetColumnFill(1, LowerRightXY.X - UpperLeftXY.X);
GridPanel->SetColumnFill(2, 1.0 - LowerRightXY.X);
}
void UlxUtilityLibrary::GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY)
{
TArray<float> &Col = GridPanel->ColumnFill;
TArray<float> &Row = GridPanel->RowFill;
// Set default return value for error situations.
UpperLeftXY.X = 0.0;
LowerRightXY.X = 1.0;
UpperLeftXY.Y = 0.0;
LowerRightXY.Y = 1.0;
if ((GridPanel == nullptr) || (Row.Num() != 3) || (Col.Num() != 3))
{
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
return;
}
double TotalX = Col[0] + Col[1] + Col[2];
double TotalY = Row[0] + Row[1] + Row[2];
if (TotalX > 0)
{
UpperLeftXY.X = Col[0] / TotalX;
LowerRightXY.X = (Col[0] + Col[1]) / TotalX;
}
if (TotalY > 0)
{
UpperLeftXY.Y = Row[0] / TotalY;
LowerRightXY.Y = (Row[0] + Row[1]) / TotalY;
}
CanvasSlot->SetAnchors(Anchors);
CanvasSlot->SetAlignment(Alignment);
CanvasSlot->SetPosition(Position);
CanvasSlot->SetSize(Size);
CanvasSlot->SetAutoSize(SizeToContent);
}
ElxUsedOrNotUsed UlxUtilityLibrary::IsKeyUsedByMappingContext(const FKey &Key, const UInputMappingContext *MappingContext)
@@ -278,22 +242,3 @@ void UlxUtilityLibrary::ValidateLuaExpr(
Status = w.ValidateLuaExpr(Code, ErrorMessage);
}
ElxControllerType UlxUtilityLibrary::DetectControllerType(ULocalPlayer *Player)
{
UInputDeviceSubsystem *IDS = GEngine->GetEngineSubsystem<UInputDeviceSubsystem>();
if (!IDS) return ElxControllerType::KeyboardMouse;
FHardwareDeviceIdentifier Device = IDS->GetMostRecentlyUsedHardwareDevice(Player->GetPlatformUserId());
if (Device.PrimaryDeviceType != EHardwareDevicePrimaryType::Gamepad)
{
return ElxControllerType::KeyboardMouse;
}
FString DeviceName = Device.HardwareDeviceIdentifier.ToString();
if (DeviceName.Contains(TEXT("PS4")) || DeviceName.Contains(TEXT("PS5")) || DeviceName.Contains(TEXT("PlayStation")))
{
return ElxControllerType::PlayStationGamepad;
}
return ElxControllerType::XboxGamepad;
}

View File

@@ -7,6 +7,7 @@
#include "Input/Events.h"
#include "Common.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Components/CanvasPanelSlot.h"
#include "UtilityLibrary.generated.h"
@@ -91,51 +92,15 @@ public:
ETraceTypeQuery TraceChannel, bool bTraceComplex, EDrawDebugTrace::Type DrawDebugType, bool bIgnorePlayerPawn,
const TArray<AActor*>& ActorsToIgnore, FHitResult& HitResult);
// Set Position of GridPanel Middle Cell
//
// Sometimes, you want to specify the position of a widget, and you
// don't want to specify the position in slate units, instead, you
// want to specify the position using fractions: ie, (0,0) is the
// upper left corner of the screen, and (1,1) is the lower-right corner.
//
// One way to accomplish this is to put your widget in the middle cell
// of a 3x3 GridPanel. Then, you can position it by adjusting the grid
// fill rules. This utility routine can do the math necessary to
// correctly populate those fill rules.
//
// This routine must be passed a 3x3 GridPanel. This will reposition
// the middle cell. You must specify the upper-left and lower-right
// corners of the middle cell as fractions between (0,0) and (1,1).
//
// Be aware that if the content of a grid cell overflows the amount of
// space allocated for it, then the grid will adjust to make room.
// But that will mean that the grid is no longer faithful to the
// positions specified in its fill rules. One way to ensure that the
// grid remains faithful to its fill rules is to put an Overlay
// into the GridPanel cell, then put -1000 padding into the
// Overlay's GridPanel slot, then put +1000 padding into the Overlay
// slot. The two paddings cancel each other out, leaving the item in
// the Overlay at the originally-intended position. But if the item
// in the overlay overflows, it doesn't cause the Grid to deform.
// Instead, the item exceeds the bounds of the grid cell, but it leaves
// the grid cell where it belongs.
//
UFUNCTION(BlueprintCallable, Category="Widget")
static void SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY);
// Get Position of GridPanel Middle Cell
// Configure a CanvasPanelSlot's parameters in a single call.
//
// The routine must be passed a 3x3 GridPanel. This will return the
// position of the middle cell of the gridpanel, expressed on a scale
// from (0,0) to (1,1).
//
// The numbers returned by this routine are based entirely on the
// GridPanel fill rules. If an item in the grid is overflowing its
// allocated space, causing the grid to deform, then that won't be
// reflected in the output of this routine.
// Target must be either a UCanvasPanelSlot directly, or a UWidget whose
// Slot is a UCanvasPanelSlot. If it is neither, logs an error and
// does nothing.
//
UFUNCTION(BlueprintPure, Category="Widget")
static void GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY);
UFUNCTION(BlueprintCallable, Category = "Widget", meta = (SizeToContent = "true"))
static void ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent);
// Check if a given key is used by the specified mapping context.
//
@@ -179,9 +144,4 @@ public:
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "context"), Category = "Luprex|Utility")
static void ValidateLuaExpr(ElxLuaSyntaxCheck &Status, FString &ErrorMessage, UObject *context, const FString &Code);
// Determine what type of controller the game is currently using
//
UFUNCTION(BlueprintCallable, category="Luprex|Utility")
static ElxControllerType DetectControllerType(ULocalPlayer *Player);
};

BIN
UnrealEngine-5.5.4-release.zip LFS Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -221,7 +221,10 @@ def unzip_unreal_engine_and_apply_patch():
print(f"Unzipping {zipfn}...")
with JZipFile(zipfn) as zf:
zf.extractall(INTEGRATION)
patchfile = f"{INTEGRATION}/EnginePatches/EnginePatch"
version = zips[0].stem.split("-")[1]
patchfile = f"{INTEGRATION}/EnginePatches/EnginePatch-{version}"
if not Path(patchfile).is_file():
sys.exit(f"Cannot find engine patch: {patchfile}")
shell(extracted, f"patch -p0 < {patchfile}")
Path(extracted).rename(UNREALENGINE)
Path(touchfile).touch()
@@ -271,7 +274,7 @@ def run_unrealengine_setup_bat_replacement():
shell(UNREALENGINE, "Engine/Binaries/DotNET/GitDependencies/win-x64/GitDependencies.exe")
shell(UNREALENGINE, "Engine/Extras/Redist/en-us/UEPrereqSetup_x64.exe /quiet /norestart")
else:
shell(UNREALENGINE, "Engine/Build/BatchFiles/Linux/GitDependencies.sh")
shell(UNREALENGINE, f"Engine/Build/BatchFiles/Linux/GitDependencies.sh --cache={INTEGRATION}/.gitdeps-cache")
shell(f"{UNREALENGINE}/Engine/Build/BatchFiles/Linux", "./Setup.sh")
touch.write_text("Downloaded")
@@ -408,7 +411,7 @@ def build_clean():
which resets your git repository to its pristine state.
DANGER: this deletes any new source code you've created!
"""
shell(f"{INTEGRATION}/luprex", "make clean")
shell(f"{INTEGRATION}", "make -f luprex/Makefile clean")
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} {DEBUG} {INTEGRATION}/Integration.uproject -clean")
Path(f"{INTEGRATION}/.vscode/compile_commands.json").unlink(missing_ok = True)

View File

@@ -336,7 +336,7 @@ eng::string LuaCoreStack::load(LuaSlot result, std::string_view code, std::strin
const char *str = lua_tolstring(L_, -1, &len);
eng::string message(str, len);
lua_pop(L_, 1);
if (sv::has_suffix(message, "near <eof>"))
if (sv::has_suffix(message, "near <eof>") && sv::is_possible_long_lua_expression(code))
{
message = "truncated lua";
}

View File

@@ -192,7 +192,6 @@ bool PrintChanneler::channel(const PrintBuffer *printbuffer, StreamBuffer *sb) {
line_ = printbuffer->first_line();
}
while (line_ < printbuffer->first_unchecked()) {
sb->write_bytes("|");
sb->write_bytes(printbuffer->nth(line_));
sb->write_bytes("\n");
line_ += 1;

View File

@@ -278,7 +278,6 @@ void SourceDB::update(const util::LuaSourceVec &source) {
for (int i = 0; i < int(source.size()); i++) {
const eng::string &file = source[i].first;
const eng::string &code = source[i].second;
util::dprint("Compiling ", file);
LS.newtable(info);
LS.rawset(info, "name", file);
LS.rawset(info, "code", code);

View File

@@ -50,19 +50,25 @@ int traceback_coroutine(lua_State *L) {
if ((ar.currentline > 0) || (*ar.namewhat != 0) || (*ar.what != 'C')) {
any = true;
lua_pushliteral(L, "\n\t");
lua_pushfstring(L, "%s:", ar.short_src);
if (ar.currentline > 0)
lua_pushfstring(L, "%d:", ar.currentline);
if (strcmp(ar.short_src, "<console>")==0)
{
lua_pushstring(L, "in the console");
}
else
{
lua_pushfstring(L, "in %s", ar.short_src);
if (ar.currentline > 0)
lua_pushfstring(L, " line %d", ar.currentline);
}
if (*ar.namewhat != '\0') /* is there a name? */
lua_pushfstring(L, " in function " LUA_QS, ar.name);
else {
if (*ar.what == 'm') /* main? */
lua_pushfstring(L, " in main chunk");
lua_pushfstring(L, " in top-level expression ");
else if (*ar.what == 'C' || *ar.what == 't')
lua_pushliteral(L, " ?"); /* C function or tail call */
lua_pushliteral(L, " in unknown C function");
else
lua_pushfstring(L, " in function <%s:%d>",
ar.short_src, ar.linedefined);
lua_pushfstring(L, " in function on line %d", ar.linedefined);
}
if (1 + lua_gettop(L) - top > 5) {
lua_concat(L, 1 + lua_gettop(L) - top);

View File

@@ -55,7 +55,7 @@ public:
virtual void event_access(AccessKind kind, int64_t place_id, std::string_view datapk, StreamBuffer *retpk) override {
switch (kind) {
case AccessKind::INVOKE_LUA_SOURCE: {
world_->update_source(datapk);
world_->update_source(datapk, 0);
run_unittests(world_->state());
stop_driver();
break;

View File

@@ -200,6 +200,24 @@ bool is_lua_comment(string_view s) {
return s.substr(start, 2) == "--";
}
bool is_possible_long_lua_expression(string_view s) {
read_space(s);
string_view id = read_lua_identifier(s);
if (id.empty()) return false;
if ((id == "function") || (id == "if") || (id == "while") || (id == "for") || (id == "repeat") || (id == "do")) return true;
if (id == "local")
{
read_space(s);
id = read_lua_identifier(s);
}
read_space(s);
read_prefix(s, "="); // If not present, returns false but we continue anyway.
read_space(s);
if (has_prefix(s, "[")) return true;
if (has_prefix(s, "(")) return true;
return false;
}
bool is_whitespace(string_view s) {
for (int i = 0; i < int(s.size()); i++) {
if (!ascii_isspace(s[i])) {

View File

@@ -106,6 +106,14 @@ bool is_lua_classname(string_view s);
// Return true if the line of code is a lua comment.
bool is_lua_comment(string_view s);
// Return true if the line of code could be the beginning of a long expression.
// In a read-eval-print loop, if the user types something like "function foo",
// that's not a complete lua expression. But we don't want to just print an error,
// we want to give the user a chance to continue typing so that he can turn it
// into a complete lua expression. This function returns true if the string looks
// like the beginning of a long lua expression. This is only a heuristic.
bool is_possible_long_lua_expression(string_view s);
// Return true if the line is entirely whitespace.
bool is_whitespace(string_view s);

View File

@@ -523,6 +523,9 @@ void World::probe_lua_call(int64_t actor_id, int64_t place_id, std::string_view
// This is called from World::update_source, and also
// from World::patch_source in the difference transmitter.
//
// When called from the difference transmitter, we suppress
// error messages.
//
// For the moment, errors are channeled to util::dprint,
// and 'print' statements just go to std::cerr. Neither
// of these is ideal. We need to get serious about setting
@@ -532,7 +535,7 @@ void World::probe_lua_call(int64_t actor_id, int64_t place_id, std::string_view
// some lua source file tries to modify, say, tangible state
// in top-level code.
//
bool World::rebuild_sourcedb() {
bool World::rebuild_sourcedb(int64_t actor_id) {
bool ok = true;
for (const eng::string &mod: source_db_.modules()) {
open_lthread_state(0, 0, 0, false);
@@ -540,30 +543,28 @@ bool World::rebuild_sourcedb() {
eng::string prints = lthread_prints_.str();
clear_lthread_state();
if (!err.empty()) ok = false;
if (!err.empty() || !prints.empty()) {
util::dprint("Loading Module ", mod);
if (!err.empty()) util::dprint(err);
if (!prints.empty()) util::dprint(prints);
if (actor_id >= 0) {
lthread_prints_ << "Compiling " << mod << std::endl;
if (!err.empty()) lthread_prints_ << err << std::endl;
if (!prints.empty()) lthread_prints_ << prints;
lthread_prints_to_actor(actor_id);
}
}
if (actor_id > 0) {
lthread_prints_ << (ok ? "Compilation Successful." : "Compilation Failed.") << std::endl;
lthread_prints_to_actor(actor_id);
}
return ok;
}
bool World::update_source(const util::LuaSourceVec &source) {
bool World::update_source(const util::LuaSourceVec &source, int64_t actor_id) {
assert(stack_is_clear());
source_db_.update(source);
return rebuild_sourcedb();
return rebuild_sourcedb(actor_id);
assert(stack_is_clear());
}
bool World::update_source(const util::LuaSourcePtr &source) {
if (source == nullptr) {
return false;
}
return update_source(*source);
}
bool World::update_source(std::string_view sourcepack) {
bool World::update_source(std::string_view sourcepack, int64_t actor_id) {
if (sourcepack.empty()) {
return false;
}
@@ -571,7 +572,7 @@ bool World::update_source(std::string_view sourcepack) {
StreamBuffer sb(sourcepack);
util::LuaSourceVec sv;
SourceDB::deserialize_source(&sv, &sb);
return update_source(sv);
return update_source(sv, actor_id);
} catch (const StreamException &ex) {
return false;
}
@@ -697,7 +698,7 @@ HttpServerResponse World::http_serve(const HttpParser &request) {
open_lthread_state(0, 0, 0, false);
eng::string msg = traceback_pcall(L, 1, LUA_MULTRET);
if (!msg.empty()) lthread_prints_ << msg << std::endl;
lthread_prints_to_dprint();
lthread_prints_to_actor(0);
clear_lthread_state();
// If the call threw an error, return
@@ -859,7 +860,7 @@ void World::invoke_lua_expr(int64_t actor_id, int64_t place_id, std::string_view
LuaExtStack LS(L, func);
// create the compiled closure.
eng::string error = LS.load(func, datapack, "=invoke");
eng::string error = LS.load(func, datapack, "<console>");
if (!error.empty()) {
// The closure is actually an error message. Do nothing.
// This should normally not happen: LuaConsole should filter
@@ -954,7 +955,7 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_vi
bool brand_new = (source_db_.modules().size() == 1);
// Compile and load the source.
bool success = update_source(datapack);
bool success = update_source(datapack, actor_id);
// Call world.init
if (brand_new) {
@@ -976,7 +977,7 @@ void World::invoke_lua_source(int64_t actor_id, int64_t place_id, std::string_vi
util::dprint("You will need to fix the errors then run it manually.");
}
}
// Run the new thread and return.
assert(stack_is_clear());
}
@@ -1071,6 +1072,10 @@ void World::run_scheduled_threads() {
PrettyPrint::Indented().print(LSCO, LuaSpecial(i), &lthread_prints_);
lthread_prints_ << std::endl;
}
if (lthread_prints_.view().empty())
{
lthread_prints_ << "ok\n";
}
}
} else if (status == LUA_YIELD) {
if (is_authoritative()) {
@@ -1092,7 +1097,7 @@ void World::run_scheduled_threads() {
}
LS.rawset(threads, sched.thread_id(), LuaNil);
}
lthread_prints_to_printbuffer();
lthread_prints_to_actor(lthread_actor_id_);
clear_lthread_state();
}
}
@@ -1158,22 +1163,20 @@ void World::open_lthread_state(int64_t actor, int64_t place, int64_t thread, boo
lthread_prints_.clear();
}
void World::lthread_prints_to_printbuffer()
void World::lthread_prints_to_actor(int64_t actor_id)
{
const eng::string &output = lthread_prints_.str();
if (output.size() > 0) {
Tangible *actor = tangible_get(lthread_actor_id_);
if (actor != nullptr) {
actor->print_buffer_.add_string(output, is_authoritative());
if (actor_id >= 0) {
Tangible *actor = tangible_get(actor_id);
if (actor != nullptr) {
actor->print_buffer_.add_string(output, is_authoritative());
} else {
util::dprintview(output);
}
}
}
}
void World::lthread_prints_to_dprint()
{
const eng::string &output = lthread_prints_.str();
if (output.size() > 0) {
util::dprintview(output);
lthread_prints_.str("");
lthread_prints_.clear();
}
}

View File

@@ -292,7 +292,7 @@ void World::patch_source(StreamBuffer *sb, DebugCollector *dbc) {
DebugBlock dbb(dbc, "patch_source");
bool modified = source_db_.patch(sb, dbc);
if (modified) {
rebuild_sourcedb();
rebuild_sourcedb(-1);
DebugLine(dbc) << "Source DB rebuilt";
}
}

View File

@@ -284,19 +284,20 @@ public:
// Rebuild the global environment from the source database.
//
// Error messages go to the specified actor.
//
// Returns true if the rebuild goes without errors.
//
bool rebuild_sourcedb();
bool rebuild_sourcedb(int64_t actor_id);
// Update the source database from disk, then rebuild the global environment.
//
// Special case: if the source pointer is nullptr, does not update.
// Error messages go to the specified actor.
//
// Returns true if the update goes without errors.
//
bool update_source(const util::LuaSourceVec &source);
bool update_source(const util::LuaSourcePtr &source);
bool update_source(std::string_view sourcepk);
bool update_source(const util::LuaSourceVec &source, int64_t actor_id);
bool update_source(std::string_view sourcepk, int64_t actor_id);
// Supply an HTTP response to an outstanding HTTP request.
//
@@ -375,8 +376,13 @@ public:
std::ostream *lthread_print_stream() { return &lthread_prints_; }
void lthread_prints_to_printbuffer();
void lthread_prints_to_dprint();
// Send the lthread_prints output to the specified actor.
//
// If actor_id == (-1) prints are discarded.
// If actor_id == (0) prints go to dprint.
// Anything else, and the prints go to a specific actor.
//
void lthread_prints_to_actor(int64_t actor_id);
// Set a lua global variable.
//

View File

@@ -13,6 +13,7 @@
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <string>
#include <string_view>
#include <type_traits>

View File

@@ -34,15 +34,19 @@ function moveto(x, y)
end
function cube.lookmenu(add)
add("Cube A", function () dprint("Doing Cube A") end)
add("Cube B", function () dprint("Doing Cube B") end)
add("Cube C", function () dprint("Doing Cube C") end)
add("Cube Hi", function () dprint("Doing Cube Hi") end)
add("Cube Bye", function () dprint("Doing Cube Bye") end)
add("Cube Yo", function () dprint("Doing Cube Yo") end)
add("Cube Z", function () dprint("Doing Cube Z") end)
end
function sphere.lookhotkeys(add)
add("DPadU", "Sphere Hi", function () dprint("Doing Sphere Hi") end)
add("DPadL", "Sphere Bye", function () dprint("Doing Sphere Bye") end)
add("DPadR", "Sphere Yo", function () dprint("Doing Sphere Yo") end)
add("FaceL", "Sphere Hi", function () dprint("Doing Sphere Hi") end)
add("FaceM", "Sphere Bye", function () dprint("Doing Sphere Bye") end)
add("FaceR", "Sphere Yo", function () dprint("Doing Sphere Yo") end)
end
@@ -75,7 +79,7 @@ function engio.getlookat()
return ""
end
print("Hello from login.lua")
function jp3()
tangible.animate{tan=actor, anim={action="play", seq="jump"}}