Compare commits
42 Commits
ec983951fe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d737879ed6 | |||
| 46051526e6 | |||
| 8dab0d16b7 | |||
| 933c1ac6c3 | |||
| 521d4726ad | |||
| 2bfa3024f1 | |||
| f7983b1f02 | |||
| 36ec4a3b9b | |||
| 1c2be1b4d8 | |||
| 0b23c82e73 | |||
| b1defd821b | |||
| e17f5417f2 | |||
| c0848c2670 | |||
| 94e6385f14 | |||
| 1328f6e5f7 | |||
| 5d2377df1d | |||
| e0d45cc1db | |||
| ff9c045c8e | |||
| e669140e2c | |||
| 420ea088d7 | |||
| b00ec49e91 | |||
| 1aa888ac82 | |||
| 236693fca6 | |||
| b5e121f884 | |||
| ac4302141c | |||
| e16e0978b0 | |||
| 3be98f3617 | |||
| 3cf984ff65 | |||
| 78c85660c9 | |||
| 3e7e6a2ae4 | |||
| 9d37f02d44 | |||
| 97b5a3c593 | |||
| ae0defbad9 | |||
| 9598004e6d | |||
| 4680a0f3f4 | |||
| 3f6ef4b56c | |||
| 960abba07f | |||
| a689d59ea0 | |||
| a964211cc8 | |||
| d985a6bc55 | |||
| 4420c52b74 | |||
| 8e5d43fd24 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,6 +27,7 @@ UnrealEngine
|
||||
*.vcproj
|
||||
.ignore
|
||||
|
||||
.gitdeps-cache/**
|
||||
.vscode/**
|
||||
Config/**
|
||||
Saved/**
|
||||
@@ -52,3 +53,4 @@ GPF-output/**
|
||||
__pycache__/
|
||||
.clangd-query/
|
||||
COMMIT.txt
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ue-wingman": {
|
||||
"command": "python3",
|
||||
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
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
321
Art/radial.svg
Normal 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
304
Art/radial2.svg
Normal 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
449
Art/radial3.svg
Normal 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 |
@@ -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)
|
||||
|
||||
|
||||
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/lxGameMode.uasset
LFS
BIN
Content/Luprex/lxGameMode.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Testing/BP_Test.uasset
LFS
BIN
Content/Testing/BP_Test.uasset
LFS
Binary file not shown.
BIN
Content/Testing/WB_Test.uasset
LFS
BIN
Content/Testing/WB_Test.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/WB_Menu.uasset
LFS
BIN
Content/Widgets/WB_Menu.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
Binary file not shown.
@@ -1,172 +0,0 @@
|
||||
# Better Debugging With LLDB (in VS Code + CodeLLDB)
|
||||
|
||||
## The Problem
|
||||
|
||||
When debugging Unreal with VS Code + CodeLLDB, the **Variables** pane and
|
||||
the **Watch** pane use two completely different evaluation paths:
|
||||
|
||||
- **Variables pane** walks a tree built by lldb's *synthetic children
|
||||
providers* (Unreal's Python formatters for TArray, TMap, FName, FString,
|
||||
and now our TObjectPtr/TSharedPtr/TWeakPtr). Values are looked up by
|
||||
offset/type — no compilation. Base classes appear as named children
|
||||
("SCompoundWidget", "UWidget"); smart-pointer inners get unwrapped;
|
||||
container elements get indexed.
|
||||
|
||||
- **Watch pane** (without intervention) runs text through the `/se` (simple)
|
||||
or `/nat` (Clang) evaluator. `/nat` fails for most Unreal paths;
|
||||
`/se` transpiles to Python and walks the SBValue tree, but — as
|
||||
originally shipped — fails on pointer auto-deref and can't find base
|
||||
classes by type name.
|
||||
|
||||
When you right-click in Variables and **Add to Watch**, CodeLLDB sends
|
||||
a path like `Top.Widget` (built itself for synthetic children, uniform
|
||||
`.`, no pointer awareness). Without the fix below, that path fails in
|
||||
Watch even though it identifies a real value Variables can show.
|
||||
|
||||
## What We Did
|
||||
|
||||
All changes live in `tools/UEDataFormatter.py`, loaded via `initCommands`
|
||||
in every launch config in `Integration.code-workspace.tpl.json`.
|
||||
|
||||
### 1. Patched `codelldb.value.Value.__getattr__`
|
||||
|
||||
At module load, we monkey-patch CodeLLDB's `Value` class — the Python
|
||||
wrapper it uses inside `/se` and `/py` evaluation — to:
|
||||
|
||||
- **Auto-deref pointers** before descending into a named child.
|
||||
- **Fall back to iterating children** by `GetName()` when
|
||||
`GetChildMemberWithName` returns invalid. This catches base classes
|
||||
(which appear as named children when iterated but can't be looked up
|
||||
by name).
|
||||
|
||||
**Result:** plain "Add to Watch" from the Variables pane produces a path
|
||||
like `Top.Widget.SomeField` and Watch evaluates it correctly — same
|
||||
expandable tree you'd see in Variables. No `/py fv(...)` wrapper, no
|
||||
VS Code extension, no manual prefix typing.
|
||||
|
||||
Blast radius: every `/se` and `/py` expression that goes through
|
||||
`Value.__getattr__`. The change is strictly more permissive (paths that
|
||||
used to fail now succeed), but it is a global behavior change to
|
||||
CodeLLDB internals. Breaks if CodeLLDB renames `Value` or the
|
||||
`__sbvalue` slot; re-applied automatically on extension reload since
|
||||
our patch runs on `command script import`.
|
||||
|
||||
### 2. Synth + Summary Providers for Smart Pointers
|
||||
|
||||
UE's engine formatter covers containers and TWeakObjectPtr, but not the
|
||||
smart pointer family. We added providers for:
|
||||
|
||||
- **`TObjectPtr<T>`** — shows `nullptr` / `unresolved` / wrapped object's
|
||||
summary. Expanding flattens straight to the target's members (no
|
||||
intermediate `*DebugPtr` click).
|
||||
- **`TSharedPtr<T>`** and **`TSharedRef<T>`** — shows `nullptr` / target
|
||||
summary; expands straight to target's members.
|
||||
- **`TWeakPtr<T>`** — checks
|
||||
`WeakReferenceCount.ReferenceController.SharedReferenceCount` before
|
||||
dereferencing. Expired weak refs show `expired` rather than garbage
|
||||
from a dangling pointer.
|
||||
|
||||
All registration regexes are anchored with `^` — otherwise the greedy
|
||||
`.+` matches nested occurrences (a `TArray<TObjectPtr<X>, ...>` would
|
||||
get dispatched to the TObjectPtr provider instead of TArray).
|
||||
|
||||
### 3. Dynamic Type Resolution
|
||||
|
||||
Every launch config now sets:
|
||||
|
||||
settings set target.prefer-dynamic-value no-run-target
|
||||
|
||||
lldb reads the vtable at each polymorphic value and shows the runtime
|
||||
type's members. A `UObject*` that actually points to a `UUserWidget`
|
||||
expands to the full `UUserWidget` subtree, not just `UObject`.
|
||||
`no-run-target` avoids running code in the debuggee, which is important
|
||||
during synthesis.
|
||||
|
||||
### 4. Universal SIGTRAP Handling
|
||||
|
||||
`process handle SIGTRAP --notify false --pass false --stop false` is now
|
||||
in every launch config. Unreal raises SIGTRAP internally in a number of
|
||||
places (soft asserts, ensure-style checks); without this, the debugger
|
||||
stops constantly.
|
||||
|
||||
## Reloading Without a Session Restart
|
||||
|
||||
If you edit `tools/UEDataFormatter.py`, reload in the Debug Console:
|
||||
|
||||
script import importlib; importlib.reload(UEDataFormatter); UEDataFormatter.__lldb_init_module(lldb.debugger, {})
|
||||
|
||||
The reload re-executes module code (updating the patch and the provider
|
||||
classes). The explicit `__lldb_init_module` call re-runs the provider
|
||||
registrations — lldb only fires `__lldb_init_module` on initial import,
|
||||
not on reload.
|
||||
|
||||
## Summary of Workflow
|
||||
|
||||
| Action | Where | How |
|
||||
|---|---|---|
|
||||
| Explore a value | Variables pane | Click disclosure triangles |
|
||||
| Track a value across steps | Watch pane | Right-click variable → Add to Watch (just works) |
|
||||
| One-shot inspection | Debug Console | `v Widget.Object->SCompoundWidget.SWidget` (`v` = `frame variable`; use `->` for pointers explicitly) |
|
||||
| Reload formatter edits | Debug Console | `script import importlib; importlib.reload(UEDataFormatter); UEDataFormatter.__lldb_init_module(lldb.debugger, {})` |
|
||||
|
||||
## Notes on the Design
|
||||
|
||||
### Why this works
|
||||
|
||||
CodeLLDB's `/se` evaluator transpiles user expressions into Python that
|
||||
operates on `Value` objects. `Value.__getattr__` drives every `.field`
|
||||
access. By making that method auto-deref and iterate for base classes,
|
||||
every downstream mechanism (Watch, Debug Console, hover, conditional
|
||||
breakpoints) inherits the fix.
|
||||
|
||||
### Why `fv` is no longer needed
|
||||
|
||||
Earlier we had an `fv(path)` helper plus a plan for a companion VS Code
|
||||
extension to wrap "Add to Watch" results in `/py fv(...)`. The
|
||||
`Value.__getattr__` patch makes the default path work, so that whole
|
||||
layer is obsolete.
|
||||
|
||||
### Why not patch `SBValue` instead
|
||||
|
||||
Tempting, but much larger blast radius — affects every tool, every
|
||||
adapter, every Python script using lldb. Patching `Value` confines the
|
||||
change to CodeLLDB's expression pipeline.
|
||||
|
||||
## Ideas for Further Improvement
|
||||
|
||||
### Propose `/fv` mode to CodeLLDB upstream
|
||||
|
||||
Prefix dispatch is in CodeLLDB's Rust binary, so we can't add a new
|
||||
prefix from Python. A clean feature request would be to add `/fv` →
|
||||
`SBFrame::GetValueForVariablePath(code)` as a native evaluator. That
|
||||
would give direct access to lldb's own frame-variable walker without
|
||||
the Python/Value indirection — though it has its own limitations
|
||||
(no arithmetic, no casts).
|
||||
|
||||
### More synth providers
|
||||
|
||||
- **`TOptional<T>`** — hide the storage bytes and `bIsSet`; expose the
|
||||
contained value (or `unset`) directly.
|
||||
- **`TVariant<...>`** — expose the currently-held alternative as the
|
||||
single child.
|
||||
- **`FText`** — show the resolved localized string as the summary.
|
||||
- **`FSoftObjectPtr` / `FSoftClassPtr`** — show the asset path.
|
||||
|
||||
### Enrich TObjectPtr summary
|
||||
|
||||
When resolved, show both class and name (`UUserWidget 'W_HUD_0'`)
|
||||
instead of just the name. The class is reachable via
|
||||
`ClassPrivate->NamePrivate`.
|
||||
|
||||
### A `fdump` helper
|
||||
|
||||
A Debug Console helper that prints an entire subtree as indented text —
|
||||
useful for grabbing a snapshot of complex state into a log or comment.
|
||||
|
||||
### Get `Copy as Expression` to emit `->` for pointers
|
||||
|
||||
The path CodeLLDB builds for synthetic children uses `.` uniformly,
|
||||
regardless of whether intermediate values are pointers. That's why
|
||||
`v Top.Widget` fails but `v Top->Widget` works. A feature request to
|
||||
have CodeLLDB emit `->` when traversing a pointer would make paths
|
||||
`frame variable`-compatible out of the box.
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
# Ideas for BlueprintMCP handler registration
|
||||
|
||||
UCLASS()
|
||||
class USpawnNodeHandler : public UMCPHandler
|
||||
{
|
||||
UPROPERTY()
|
||||
FString BlueprintName;
|
||||
|
||||
UPROPERTY();
|
||||
FString GraphName;
|
||||
|
||||
UPROPERTY();
|
||||
FString ActionName;
|
||||
|
||||
UPROPERTY(meta = "optional");
|
||||
int PosX;
|
||||
|
||||
UPROPERTY(meta = "optional");
|
||||
int PosY;
|
||||
|
||||
UPROPERTY()
|
||||
ElxLuaValueType ValueType;
|
||||
|
||||
// Dummy field. Nothing is actually extracted by the argument parser,
|
||||
// but this tells the argument parser that the parameter "subtree" is
|
||||
// supposed to be there.
|
||||
UPROPERTY()
|
||||
void Subtree;
|
||||
|
||||
virtual void Handle(Json, Result) override;
|
||||
};
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Blueprint Text Export
|
||||
|
||||
Blueprints are stored as binary `.uasset` files that Claude Code cannot read directly. To work around this, a text exporter automatically converts blueprint graphs to readable text files whenever a blueprint is saved in the Unreal editor.
|
||||
|
||||
## How It Works
|
||||
|
||||
The game module (`FlxIntegrationModuleImpl` in `Source/Integration/Integration.cpp`) hooks into `UPackage::PackageSavedWithContextEvent`. When a blueprint is saved, it iterates each `UEdGraph` in the blueprint and runs `FlxBlueprintExporter` on it. The output is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`.
|
||||
|
||||
The exporter class (`Source/Integration/BlueprintExporter.h/.cpp`) processes one graph at a time. The constructor runs all passes and the result is available via `GetOutput()`.
|
||||
|
||||
## Output Format
|
||||
|
||||
The graph file is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`. A details file with node-name-to-GUID mappings is written to `Saved/BlueprintExports/<BlueprintName>/DETAILS/<GraphName>.txt`.
|
||||
|
||||
Every line in the graph file starts with a keyword, making it easy to parse. The format is:
|
||||
|
||||
```
|
||||
node Event_Tick
|
||||
return Output_Delegate, Delta_Seconds
|
||||
goto Set_Tick_Delta_Seconds
|
||||
|
||||
node Set_Tick_Delta_Seconds
|
||||
input Real Tick_Delta_Seconds = Event_Tick.Delta_Seconds
|
||||
return Output_Get
|
||||
goto CallFunctionByName
|
||||
```
|
||||
|
||||
### Line Keywords
|
||||
|
||||
- `node Name` — starts a new node block.
|
||||
- `input Type Name = Source` — an input data pin. Source is a `Node.Pin` reference, a literal value, `<self>`, or `<default>`.
|
||||
- `return Pin1, Pin2` — output data pins.
|
||||
- `goto Target` — exec flow (single output).
|
||||
- `goto-if PinName Target` — exec flow (multiple outputs, e.g. branch true/false).
|
||||
- `// comment text` — comment node.
|
||||
|
||||
### Special Handling
|
||||
|
||||
- String defaults are shown in quotes.
|
||||
- Variable get nodes are inlined (the variable name appears directly at the point of use rather than as a separate node).
|
||||
- Knot (reroute) nodes are followed through transparently.
|
||||
|
||||
## Node Ordering
|
||||
|
||||
Nodes are sorted by a traversal algorithm: find starter nodes (exec output but no exec input), sort them by Y position, then traverse each. The traversal visits a node's inputs first (so data sources appear before their consumers), then emits the node itself, then follows exec outputs. This produces a readable top-down flow. Any unvisited nodes are appended at the end.
|
||||
|
||||
## Node Naming
|
||||
|
||||
Names are derived from `ENodeTitleType::ListView` titles, sanitized to alphanumeric plus underscores. Duplicates get `_2`, `_3`, etc.
|
||||
117
Docs/Getting-Gamepad-USB-Device-Name.md
Normal file
117
Docs/Getting-Gamepad-USB-Device-Name.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Getting the Gamepad USB Device Name
|
||||
|
||||
Unreal exposes `UInputDeviceSubsystem::GetMostRecentlyUsedHardwareDevice()`, which
|
||||
returns an `FHardwareDeviceIdentifier`. In stock Unreal this is nearly useless: on
|
||||
Windows it toggles between `"WindowsApplication"` (KBM) and `"XInputController"`
|
||||
(any XInput pad); on Linux it returns nothing meaningful at all. It cannot
|
||||
distinguish a DualSense from an Xbox pad.
|
||||
|
||||
The USB-reported device name (e.g. `"Sony Interactive Entertainment Wireless
|
||||
Controller"`) is what we want to surface. This document describes the changes
|
||||
required on each platform to put that name into `FHardwareDeviceIdentifier`.
|
||||
|
||||
## How `FHardwareDeviceIdentifier` Gets Populated
|
||||
|
||||
Input drivers wrap their `MessageHandler->OnControllerButton/Analog` calls in an
|
||||
`FInputDeviceScope` (stack-allocated, thread-local stack of pointers). The scope
|
||||
carries `InputDeviceName` (driver class) and `HardwareDeviceIdentifier` (physical
|
||||
device string). When the input event reaches `UInputDeviceSubsystem`, those two
|
||||
fields are copied into the `FHardwareDeviceIdentifier` record for that user.
|
||||
|
||||
So the fix on each platform is the same shape: **at device-connect time, cache
|
||||
the USB device-name string; in the per-event scope, pass it as the
|
||||
`HardwareDeviceIdentifier`.**
|
||||
|
||||
## Windows: Patch the GameInput Plugin
|
||||
|
||||
Location: `Engine/Plugins/Runtime/GameInput/Source/GameInputBase/`.
|
||||
|
||||
The Microsoft GameInput SDK already exposes the data we need on every device via
|
||||
`GameInputDeviceInfo::displayName` (a `wchar_t*`). The plugin reads it for log
|
||||
lines (`GameInputUtils.cpp:18-23`) but does not propagate it.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Cache at connect.** Add `FString CachedDisplayName` to
|
||||
`FGameInputDeviceContainer`. In its constructor / device-info init path
|
||||
(`GameInputDeviceContainer.cpp:44`, where `Info` is in scope), set:
|
||||
```cpp
|
||||
CachedDisplayName = FString(Info->displayName);
|
||||
```
|
||||
`TCHAR == wchar_t` on Windows, so the conversion is direct. Lifetime matches
|
||||
the container — cleared automatically on disconnect.
|
||||
|
||||
2. **Make the container reachable from `FGameInputEventParams`.** The processor
|
||||
already gets a `Device` pointer; add a `Container` pointer so
|
||||
`GetHardwareDeviceIdentifierName` can read the cached string.
|
||||
|
||||
3. **Return it from `GetHardwareDeviceIdentifierName`**
|
||||
(`GameInputDeviceProcessor.cpp:61`). After the existing
|
||||
`bOverrideHardwareDeviceIdString` block, before the family-bucket switch:
|
||||
```cpp
|
||||
if (Params.Container && !Params.Container->CachedDisplayName.IsEmpty())
|
||||
{
|
||||
return Params.Container->CachedDisplayName;
|
||||
}
|
||||
```
|
||||
The override path still wins (explicit dev intent); the family-bucket
|
||||
fallback handles devices with empty `displayName`.
|
||||
|
||||
Total: ~10 lines plus the field. All inside `#if GAME_INPUT_SUPPORT`.
|
||||
|
||||
The user must enable the GameInput plugin and disable the default `XInputDevice`
|
||||
plugin (otherwise both drivers will dispatch events for the same pad).
|
||||
|
||||
## Linux: Patch `LinuxApplication.cpp`
|
||||
|
||||
Location: `Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp`.
|
||||
|
||||
SDL already provides the device name via `SDL_GameControllerName()`. The Linux
|
||||
path calls it once for a log line in `AddGameController` (line 2175) and then
|
||||
discards it. The Linux path also does not push an `FInputDeviceScope` at all
|
||||
around its controller events — that's the second half of why
|
||||
`GetMostRecentlyUsedHardwareDevice` is useless on Linux.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Store the name.** Add `FString DeviceName` to `SDLControllerState`. In
|
||||
`AddGameController` (~line 2175), assign:
|
||||
```cpp
|
||||
ControllerState.DeviceName = UTF8_TO_TCHAR(SDL_GameControllerName(Controller));
|
||||
```
|
||||
|
||||
2. **Wrap controller-event dispatch in a scope.** Around the
|
||||
`MessageHandler->OnControllerButton*/OnControllerAnalog` block (starting
|
||||
~line 518):
|
||||
```cpp
|
||||
FInputDeviceScope Scope(
|
||||
nullptr,
|
||||
FName("LinuxApplication"),
|
||||
ControllerState.DeviceId.GetId(),
|
||||
ControllerState.DeviceName);
|
||||
// ... existing dispatch ...
|
||||
```
|
||||
Use `"LinuxApplication"` for `InputDeviceName` to mirror the Windows
|
||||
convention.
|
||||
|
||||
Total: ~12 lines. No third-party dependencies, no engine plugins, no per-device
|
||||
config tables.
|
||||
|
||||
## What This Does Not Solve
|
||||
|
||||
- **Stock XInput on Windows.** If we keep the default `XInputDevice` plugin,
|
||||
events continue to come through `XInputInterface.cpp` with a fixed
|
||||
`"XInputController"` identifier. XInput itself does not expose VID/PID or
|
||||
device strings. Recovering the name there requires correlating the XInput
|
||||
slot with a Raw Input HID via the `IG_xx` token in the Raw Input device path,
|
||||
then calling `HidD_GetProductString` — a separate, more involved patch.
|
||||
GameInput is the cleaner answer on Windows.
|
||||
|
||||
- **XInput-wrapped pads under GameInput.** A DualSense routed through Steam
|
||||
Input or DS4Windows will surface as the wrapper's display name (e.g. ViGEm),
|
||||
not as the underlying physical device. This is a property of the wrapper, not
|
||||
fixable from our side.
|
||||
|
||||
- **`displayName` is not a stable identifier.** It is a human-readable name
|
||||
whose exact text varies by OS version, driver, and USB descriptor. Use it for
|
||||
prompt-set selection and logging. Do not use it as a save-game key.
|
||||
311
Docs/Luprex-Window-Management.md
Normal file
311
Docs/Luprex-Window-Management.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Introduction
|
||||
|
||||
Unreal has several input mode-related subsystems that
|
||||
interact with each other in complicated ways. These
|
||||
subsystems include:
|
||||
|
||||
- keyboard focus
|
||||
- mouse capture
|
||||
- enhanced input routing
|
||||
- pointer visibility
|
||||
- window z-order
|
||||
|
||||
Unreal is littered with conditionals that cause these bits
|
||||
of state to affect each other in unpredictable, often
|
||||
illogical ways. If you set these bits of state in the wrong
|
||||
order, or to the wrong values, it is all too easy to get
|
||||
unreal into a non-functioning state. The system is *much*
|
||||
too fragile.
|
||||
|
||||
For this reason, I have implemented a window management
|
||||
system that orchestrates all of this from a centralized
|
||||
location, in a way that guarantees reasonably predictable,
|
||||
sane behavior.
|
||||
|
||||
# Core Design Choices
|
||||
|
||||
Our window management system, in order to keep things
|
||||
simple, has to make some assumptions about how Luprex games
|
||||
work. So, here are the rules.
|
||||
|
||||
Top-level UserWidgets get inserted into a "Root Canvas",
|
||||
instead of into the viewport. The root canvas implements most
|
||||
of the functionality of our window management system.
|
||||
|
||||
The keyboard focus rule is simple: the UserWidget in front
|
||||
according to the z-order gets keyboard focus. The window
|
||||
management system will put focus on the front widget and
|
||||
will keep it there. The *only* way to give a UserWidget
|
||||
keyboard focus is to raise it to the front of the z-order.
|
||||
|
||||
Mouse movements events are handled in two different ways:
|
||||
the system can shift between "mouselook" mode and
|
||||
"point-and-click" mode. Every top-level UserWidget declares
|
||||
whether it wants a mouse pointer or not. If the front
|
||||
UserWidget wants a pointer, the system shifts into
|
||||
point-and-click mode.
|
||||
|
||||
In point-and-click mode, enhanced input mouse move events
|
||||
cannot happen. In mouselook mode, widget OnMouseDown and
|
||||
OnMouseMove events cannot happen. In both modes, you can
|
||||
track mouse movement, but you have to use different
|
||||
mechanisms.
|
||||
|
||||
Widgets that declare that they want a pointer are
|
||||
automatically put in front of widgets that don't want a
|
||||
pointer. Because of this rule, the system essentially
|
||||
separates into the "mouselook" layer underneath, and the
|
||||
"point-and-click" layer on top. When the point-and-click
|
||||
layer gets out of the way, then you can drive the 3D world.
|
||||
|
||||
# State Variables of the Window Management System
|
||||
|
||||
I have made an effort to keep the number of state variables
|
||||
that you have to control to an absolute minimum, and to
|
||||
concentrate them all in one place. That place is the "Root
|
||||
Canvas Slot."
|
||||
|
||||
Typically, in Unreal, when you create a new top-level
|
||||
widget, you insert it into the viewport using AddToViewport.
|
||||
But to use our window management system, you must instead
|
||||
insert top-level widgets into a 'root canvas', using
|
||||
AddWidgetToRoot.
|
||||
|
||||
The root canvas object associates a RootCanvasSlot to each
|
||||
top-level widget. The RootCanvasSlot is a place where we can
|
||||
store window management-related hints for that widget. The
|
||||
contents of the RootCanvasSlot include the following:
|
||||
|
||||
- `ShowPointer`: If true, this is a point-and-click widget.
|
||||
When this widget is in front, the pointer is visible,
|
||||
and the system switches to point-and-click mode.
|
||||
|
||||
- `BlockInput`: If this window is in front, all enhanced
|
||||
input events in *other* objects are blocked.
|
||||
|
||||
- `EnableEnhancedInput`: If false, enhanced input events in
|
||||
*this* widget are disabled.
|
||||
|
||||
- `BringToFrontCount`: Effectively, a timestamp indicating
|
||||
the last time this window was brought to the front. This
|
||||
is the main factor determining the z-order of the widgets.
|
||||
|
||||
In addition, the top-level widget itself contains some
|
||||
window-management related properties. Currently, these are:
|
||||
|
||||
- `DesiredFocusWidget`: Indicates which sub-widget, if any,
|
||||
should be given focus. When the system grants focus to
|
||||
the frontmost UserWidget, the focus actually goes here.
|
||||
|
||||
There are deliberately *no other variables* that control our
|
||||
new window management system. If your blueprint is managing
|
||||
these properties, then it is doing everything it needs to
|
||||
do.
|
||||
|
||||
The function SetWidgetWindowManagement can set all of these
|
||||
variables in a single operation. That one function is all
|
||||
you need to control the entire window management system.
|
||||
|
||||
# Handling Keyboard and Gamepad Buttons
|
||||
|
||||
Here is a summary of how keyboard/gamepad button handling in
|
||||
unreal works. We have tweaked this slightly, but this is
|
||||
mostly just ordinary unreal input handling:
|
||||
|
||||
When you press a keyboard or gamepad button, the button
|
||||
first goes to any widget that has keyboard focus. If that
|
||||
widget doesn't declare the button to be "handled", then
|
||||
button is offered to other widgets higher in the widget
|
||||
heirarchy. If no widget handles the button, the button then
|
||||
goes to the "enhanced input subsystem."
|
||||
|
||||
The enhanced input system puts the button through
|
||||
an "input mapping context." Basically, that's a many-to-one
|
||||
map that translates buttons into more abstract "enhanced
|
||||
input events." Here's a fragment of a typical input
|
||||
mapping context:
|
||||
|
||||
Key W --> IA_Move_Forward
|
||||
Key S --> IA_Move_Backward
|
||||
Left_Thumbstick_Forward --> IA_Move_Forward
|
||||
Left_Thumbstick_Backward --> IA_Move_Backward
|
||||
|
||||
What the mapping context buys you is that you can handle
|
||||
events like "IA_Move_Forward" without having to care
|
||||
whether the player is driving with the WASD keys or with
|
||||
the gamepad left thumbstick.
|
||||
|
||||
Typically, enhanced input events go to *all* of the
|
||||
following: the player controller, the character, and
|
||||
user-defined widgets. All of these consumers of enhanced
|
||||
input are automatically registered to receive enhanced
|
||||
input, which means that all they have to do is implement a
|
||||
handler in their event graph, and they're ready. Other
|
||||
actors can *also* receive enhanced input, but that requires
|
||||
jumping through some hoops.
|
||||
|
||||
It's interesting that a widget can implement a handler for a
|
||||
raw keyboard button, and then declare the button "not
|
||||
handled". If the button proceeds to the enhanced input
|
||||
system, and if the widget has a handler for enhanced input,
|
||||
the widget can receive the same button again, in a
|
||||
different form!
|
||||
|
||||
There is a priority order among consumers of enhanced input
|
||||
events: user widgets first (front-to-back), then the player
|
||||
controller, then the character. A consumer of enhanced
|
||||
input has the option of blocking input to lower-priority
|
||||
consumers.
|
||||
|
||||
This is all almost entirely unchanged from Unreal's default
|
||||
behavior. We've only made two tiny tweaks: we send enhanced
|
||||
input to widgets in front-to-back order, and, widgets
|
||||
disable enhanced input by setting a flag instead of by
|
||||
unregistering their input component. Other than that, this
|
||||
is all just stock unreal.
|
||||
|
||||
# Handling mouse buttons
|
||||
|
||||
Mouse buttons behave differently than keyboard buttons.
|
||||
|
||||
Widgets have an OnMouseDown handler. This is only active in
|
||||
point-and-click mode. OnMouseDown only fires when three
|
||||
things are true: the system must be in point-and-click mode,
|
||||
the pointer must be inside the rectangle of a widget, and
|
||||
the widget must be marked hit-testable.
|
||||
|
||||
If no OnMouseDown event fires, or if OnMouseDown declares
|
||||
the mouse down to be "not handled," then the mouse down
|
||||
makes it to the enhanced input subsystem.
|
||||
|
||||
Once the mouse down reaches the enhanced input system, it
|
||||
starts being treated the same as keyboard and gamepad
|
||||
buttons. It can be mapped to an enhanced input event by the
|
||||
input mapping context, and then from there, it can be
|
||||
handled by any enhanced input event handler in a blueprint.
|
||||
|
||||
The upshot of all this is: if you want to think of a mouse
|
||||
button as "just another button," then the way to achieve
|
||||
that is to *not* write an OnMouseDown handler, but instead,
|
||||
to deal with it through enhanced input.
|
||||
|
||||
We have tweaked the default behavior of unreal. If the
|
||||
system is in point-and-click mode, and you click on a widget
|
||||
that is hit-testable, but which has no OnMouseDown handler,
|
||||
we provide a default OnMouseDown behavior: we bring the
|
||||
top-level UserWidget to the front. Because our system grants
|
||||
keyboard focus to the widget in front, this will also grant
|
||||
focus.
|
||||
|
||||
# Handling Mouse Movement
|
||||
|
||||
In point-and-click mode, mouse movement moves the pointer
|
||||
and doesn't generate any events at all.
|
||||
|
||||
There is one exception: mouse capture. If you click on a
|
||||
hit-testable widget, that widget will "capture" the mouse
|
||||
until you release the mouse button. As long as the widget
|
||||
has capture, it receives OnMouseMove events. This is
|
||||
mainly intended to implement click-and-drag, scroll
|
||||
bar scrolling, and other movements like that.
|
||||
|
||||
Unreal has a *lot* of complicated mouse capture and mouse
|
||||
lock options and modes. We don't support any of that. We
|
||||
support only the basics: automatic capture when you click.
|
||||
If you need more, we'll have to improve the Luprex window
|
||||
management system.
|
||||
|
||||
In point-and-click mode, mouse movements do not go to the
|
||||
enhanced input system at all.
|
||||
|
||||
When the system is in mouselook mode, mouse movements go
|
||||
directly to the enhanced input system. They get mapped by
|
||||
the input mapping context and turned into enhanced input
|
||||
events. Handling these events is how mouselook works.
|
||||
|
||||
# Handling Analog Joysticks
|
||||
|
||||
Analog joysticks (including gamepad thumbsticks) generate
|
||||
events that go directly to the enhanced input subsystem.
|
||||
They get mapped to enhanced input events. From there,
|
||||
they can be handled by any consumer of enhanced input.
|
||||
|
||||
# Functions you Should NOT CALL!
|
||||
|
||||
If you're using our Luprex window management system, there are
|
||||
several things your blueprint should *NOT* do:
|
||||
|
||||
- DO NOT use SetKeyboardFocus, SetUserFocus, or any other
|
||||
function with Set-Focus in the name. Instead, just
|
||||
be aware that the frontmost UserWidget will get focus.
|
||||
It can delegate that focus to one of its components by
|
||||
setting DesiredFocusWidget.
|
||||
|
||||
- DO NOT use SetShowMouseCursor, or set the bShowMouseCursor
|
||||
flag. Instead, set the ShowPointer flag in the
|
||||
RootCanvasSlot of any top-level widget.
|
||||
|
||||
- DO NOT use UserWidget::RegisterInputComponent or
|
||||
UserWidget::UnregisterInputComponent. These will be
|
||||
ignored. Instead, set or unset the flag
|
||||
EnableEnhancedInput in the RootCanvasSlot, which
|
||||
effectively does the same thing.
|
||||
|
||||
- DO NOT use SetZOrder. If you try, you will be overridden
|
||||
by our window management code. Currently, the only control
|
||||
we're giving over window z-order is 'BringToFront'. If
|
||||
you need more control, we'll have to enhance the window
|
||||
management system.
|
||||
|
||||
- DO NOT use SetMouseCaptureMode, SetMouseLockMode,
|
||||
SetHideCursorDuringCapture, CaptureMouse, ReleaseMouseCapture,
|
||||
LockMouseToWidget, ReleaseMouseLock. We simply don't support
|
||||
controlling mouse capture and mouse lock at this level of
|
||||
granularity. Trying to use these will fight our window
|
||||
management code. If you need this, we'll have to enhance the
|
||||
window management system.
|
||||
|
||||
- DO NOT use AddToViewport or AddToPlayerScreen. Top level
|
||||
UserWidgets should be inserted into the root canvas using
|
||||
AddWidgetToRoot.
|
||||
|
||||
- DO NOT use SetIgnoreInput. You will be overridden. Our
|
||||
window management system relies on the enhanced input
|
||||
system being active, turning it off would cause everything
|
||||
to fail. However, a widget can handle keyboard or
|
||||
character events, causing them not to be propagated, it
|
||||
can also block events to any widget lower in the z-order,
|
||||
and to the player controller and character.
|
||||
|
||||
- DO NOT use SetInputModeXXX. Be aware that there is no
|
||||
"input mode" enum or "input mode" variable anywhere in
|
||||
Unreal. What these functions actually do is set a large
|
||||
number of state variables - keyboard focus, mouse capture,
|
||||
and so forth - from a single call. Naturally, then, these
|
||||
will fight our window management system.
|
||||
|
||||
# Most *local* event-handling functions are allowed
|
||||
|
||||
There are many functions that gate or route events locally -
|
||||
ie, within a single UserWidget, or within a single Actor.
|
||||
Controlling and gating events within a single localized
|
||||
entity does not create window-management confusion. Because
|
||||
of that, all of these are still allowed:
|
||||
|
||||
- You CAN use EnableInput/DisableInput on actors, to turn
|
||||
enhanced input events on/off for that actor.
|
||||
|
||||
- You CAN use PushInputComponent/PopInputComponent on the
|
||||
player controller, if you want to register something that's
|
||||
NOT a widget to receive enhanced input events. Seems
|
||||
esoteric, but it still works.
|
||||
|
||||
- You CAN use methods of UUserWidget to bind or unbind
|
||||
input events.
|
||||
|
||||
More broadly, functions that an actor or widget uses to
|
||||
manipulate its *own* input component or input events
|
||||
are no problem.
|
||||
|
||||
|
||||
|
||||
27
Docs/TASKS/Dictation.txt
Normal file
27
Docs/TASKS/Dictation.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
I need you to act as a secretary taking dictation. You will
|
||||
be helping me to edit a markdown file.
|
||||
|
||||
I have a voice-to-speech program which is running in the background.
|
||||
It records, and it sends my words to you. Most of what I say to you
|
||||
will be meant as text to be put into the markdown file. But
|
||||
occasionally, I will give you verbal instructions, for example:
|
||||
|
||||
"Reformat that bullet list into a numbered list"
|
||||
|
||||
It is your job to figure out intelligently which of the things I
|
||||
say to you is meant as a directive, and which is meant as words to
|
||||
go into the markdown file.
|
||||
|
||||
Do not edit the markdown file after every sentence. Instead,
|
||||
quietly listen until you have a significant edit to make. I'd say,
|
||||
roughly, when you have a paragraph, then it's time to make an edit.
|
||||
|
||||
I may occasionally commandeer the keyboard and edit the markdown
|
||||
file myself. In those cases, you should notice that the file changed,
|
||||
and read my changes.
|
||||
|
||||
It is also your job to make small corrections without comment.
|
||||
If you see a really big mistake, stop and ask me what to do.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
286
EnginePatches/EnginePatch-5.7.4
Normal file
286
EnginePatches/EnginePatch-5.7.4
Normal 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);
|
||||
47
EnginePatches/Old-Patches.md
Normal file
47
EnginePatches/Old-Patches.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Old Patches
|
||||
|
||||
Patches that were once part of `EnginePatch` but are no longer needed.
|
||||
Kept here for reference in case the underlying issue resurfaces.
|
||||
|
||||
## LinuxWindow.cpp — force override_redirect for borderless child windows
|
||||
|
||||
Removed when the engine was upgraded to UE 5.7, which uses SDL3. SDL3 has
|
||||
better Wayland support, so this workaround is believed to be unnecessary.
|
||||
The original purpose was to make Unreal play better with Wayland: 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 (notification popups,
|
||||
dialogs) also needed it, so we temporarily enabled
|
||||
`SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT` around the `SDL_CreateWindow` call.
|
||||
|
||||
```diff
|
||||
--- 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)
|
||||
{
|
||||
```
|
||||
@@ -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++",
|
||||
@@ -50,6 +51,9 @@
|
||||
],
|
||||
"C_Cpp.autocomplete": "disabled",
|
||||
"search.useIgnoreFiles": true,
|
||||
"files.readonlyInclude": {
|
||||
"[UNREALENGINE]/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/Intermediate": true,
|
||||
"**/Saved": true,
|
||||
@@ -127,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()\""
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.py"
|
||||
|
||||
* The python program "ue-wingman-mcp.py", which an AI
|
||||
can use to send commands to the plugin.
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -281,7 +281,7 @@ public:
|
||||
}
|
||||
|
||||
// Set the 'Class' property.
|
||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory);
|
||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory, true);
|
||||
FWingProperty::Remove(Props, TEXT("BlueprintType"));
|
||||
if (Props.Num() != 1)
|
||||
{
|
||||
|
||||
@@ -37,5 +37,6 @@ public:
|
||||
if (!P) return;
|
||||
|
||||
WingOut::Stdout.Print(P->GetText());
|
||||
WingOut::Stdout.Print(TEXT("\n"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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(&Entry);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(&Entry);
|
||||
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"));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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(&Entry);
|
||||
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());
|
||||
}
|
||||
};
|
||||
@@ -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(&Entry);
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "WingFetcher.h"
|
||||
#include "WingServer.h"
|
||||
#include "WingBasics.h"
|
||||
#include "WingProperty.h"
|
||||
#include "WingUtils.h"
|
||||
#include "WingComponent.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
@@ -368,36 +369,27 @@ WingFetcher& WingFetcher::StructProp(const FString& Value)
|
||||
return SetError();
|
||||
}
|
||||
|
||||
FStructProperty* StructProp = nullptr;
|
||||
TArray<FWingProperty> Details =
|
||||
FWingProperty::GetDetails(Obj.Get(), true);
|
||||
|
||||
// The "host" is the object containing this property.
|
||||
UObject *HostObject = Obj.Get();
|
||||
void *HostBase = Obj.Get();
|
||||
UStruct *HostType = Obj.Get()->GetClass();
|
||||
bool HostEditable = true;
|
||||
FWingProperty *WProp = WingUtils::FindOneWithInternalID(
|
||||
InternalID, Details, TEXT("Property"), WingOut::Stdout);
|
||||
if (!WProp) return SetError();
|
||||
|
||||
// If we are *already* inside a UWingStructRef, update the host
|
||||
// fields, to make it possible to navigate even further inside.
|
||||
if (UWingStructRef *SPtr = ::Cast<UWingStructRef>(Obj.Get()))
|
||||
if (FStructProperty *FSProp = CastField<FStructProperty>(WProp->Prop))
|
||||
{
|
||||
HostObject = SPtr->Object;
|
||||
HostBase = SPtr->StructBase;
|
||||
HostType = SPtr->StructType;
|
||||
HostEditable = SPtr->Editable;
|
||||
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
||||
Ptr->Object = WProp->Object.Get();
|
||||
Ptr->StructType = FSProp->Struct;
|
||||
Ptr->StructBase = FSProp->ContainerPtrToValuePtr<void>(WProp->Container);
|
||||
Ptr->Editable = WProp->Editable;
|
||||
SetObj(Ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
|
||||
if (!StructProp)
|
||||
else
|
||||
{
|
||||
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->GetName());
|
||||
Errors.Printf(TEXT("Property %s is not a struct property.\n"),
|
||||
*WProp->Prop->GetName());
|
||||
return SetError();
|
||||
}
|
||||
|
||||
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
||||
Ptr->Object = HostObject;
|
||||
Ptr->StructType = StructProp->Struct;
|
||||
Ptr->StructBase = StructProp->ContainerPtrToValuePtr<void>(HostBase);
|
||||
Ptr->Editable = HostEditable && StructProp->HasAllPropertyFlags(CPF_Edit);
|
||||
SetObj(Ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -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   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   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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ bool FWingParameterEditor::AddOverride(
|
||||
|
||||
// Parse the value string.
|
||||
FWingParameterEditor Editor;
|
||||
if (!FWingProperty(GS->Property, &Editor, true).SetText(StrVal, Errors)) return false;
|
||||
if (!FWingProperty(nullptr, &Editor, GS->Property, true).SetText(StrVal, Errors)) return false;
|
||||
Meta->Value = GS->Getter(Editor);
|
||||
|
||||
// Apply the update.
|
||||
@@ -308,7 +308,7 @@ void FWingParameterEditor::Print(const Info &ID, const Metadata &Meta)
|
||||
}
|
||||
FWingParameterEditor Editor;
|
||||
GS->Setter(Editor, Meta.Value);
|
||||
FString StrVal = FWingProperty(GS->Property, &Editor, false).GetText();
|
||||
FString StrVal = FWingProperty(nullptr, &Editor, GS->Property, false).GetText();
|
||||
WingOut::Stdout.Printf(TEXT(" %s %s\n"),
|
||||
*StringID(ID), *StrVal);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -378,25 +327,25 @@ FString FWingProperty::GetCategory() const
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FWingProperty> FWingProperty::GetAll(FWingStructAndUStruct Obj)
|
||||
TArray<FWingProperty> FWingProperty::GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
|
||||
{
|
||||
TArray<FWingProperty> Result;
|
||||
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
|
||||
for (TFieldIterator<FProperty> It(Struct); It; ++It)
|
||||
{
|
||||
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
|
||||
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||
Result.Emplace(Obj, Container, *It, Editable);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<FWingProperty> FWingProperty::GetVisible(FWingStructAndUStruct Obj)
|
||||
TArray<FWingProperty> FWingProperty::GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
|
||||
{
|
||||
TArray<FWingProperty> Result;
|
||||
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
|
||||
for (TFieldIterator<FProperty> It(Struct); It; ++It)
|
||||
{
|
||||
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
|
||||
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
|
||||
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||
Result.Emplace(Obj, Container, *It, Editable);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
@@ -436,10 +385,8 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
||||
// of the struct instead. Propagate editability of the host.
|
||||
if (UWingStructRef *SP = Cast<UWingStructRef>(Obj))
|
||||
{
|
||||
TArray<FWingProperty> Result =
|
||||
GetVisible(FWingStructAndUStruct(SP->StructBase, SP->StructType));
|
||||
if (!Mutable || (!SP->Editable)) StripEditable(Result);
|
||||
return Result;
|
||||
return GetVisible(SP->Object, SP->StructBase,
|
||||
SP->StructType, Mutable && SP->Editable);
|
||||
}
|
||||
|
||||
// Blueprints don't have editable properties. So
|
||||
@@ -465,7 +412,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FWingProperty> Result = GetVisible(Obj);
|
||||
TArray<FWingProperty> Result = GetVisible(Obj, Mutable);
|
||||
|
||||
// If it's a Material Graph node, also collect properties from
|
||||
// the associated material expression.
|
||||
@@ -474,7 +421,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
||||
{
|
||||
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
|
||||
{
|
||||
Result.Append(GetVisible(Expr));
|
||||
Result.Append(GetVisible(Expr, Mutable));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,66 +433,63 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
||||
FWingProperty::Remove(Result, TEXT("Slot"));
|
||||
if (UPanelSlot* Slot = Widget->Slot)
|
||||
{
|
||||
Result.Append(GetVisible(Slot));
|
||||
Result.Append(GetVisible(Slot, Mutable));
|
||||
}
|
||||
FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable"));
|
||||
if (VarProp) Result.Add(FWingProperty(VarProp, Widget, true));
|
||||
if (VarProp) Result.Emplace(Widget, Widget, VarProp, true);
|
||||
}
|
||||
|
||||
if (!Mutable) StripEditable(Result);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -580,11 +524,6 @@ bool FWingProperty::CheckImportTextResult(const FString &Value, WingOut Errors)
|
||||
return true;
|
||||
}
|
||||
|
||||
void FWingProperty::StripEditable(TArray<FWingProperty> &Props)
|
||||
{
|
||||
for (FWingProperty &Elt : Props) Elt.Editable = false;
|
||||
}
|
||||
|
||||
bool FWingProperty::CheckEditable(WingOut Errors) const
|
||||
{
|
||||
if (!Editable)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Misc/CoreDelegates.h"
|
||||
#include "Misc/OutputDeviceRedirector.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
@@ -55,8 +56,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
|
||||
return;
|
||||
}
|
||||
|
||||
BuildWingHandlerRegistry();
|
||||
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddUObject(this, &UWingServer::OnModulesChanged);
|
||||
LoadingPhasesCompleteHandle = FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UWingServer::BuildWingHandlerRegistry);
|
||||
LogCapture.bEnabled = false;
|
||||
GLog->AddOutputDevice(&LogCapture);
|
||||
bRunning = true;
|
||||
@@ -65,7 +65,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
|
||||
|
||||
void UWingServer::Deinitialize()
|
||||
{
|
||||
FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle);
|
||||
FCoreDelegates::OnAllModuleLoadingPhasesComplete.Remove(LoadingPhasesCompleteHandle);
|
||||
|
||||
if (!bRunning)
|
||||
{
|
||||
@@ -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")});
|
||||
TArray<FString> Argv;
|
||||
FString ResponseText;
|
||||
|
||||
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
|
||||
if (!Value->TryGetObject(RequestPtr))
|
||||
return PackageResponses({TEXT("Json must be an object")});
|
||||
TSharedPtr<FJsonObject> Request = *RequestPtr;
|
||||
|
||||
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.
|
||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler);
|
||||
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
|
||||
// Populate the handler object with argv parameters.
|
||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
||||
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);
|
||||
@@ -534,11 +495,6 @@ void UWingServer::BuildWingHandlerRegistry()
|
||||
WingHandlerRegistry.Sort([](const FWingHandlerConfig& A, const FWingHandlerConfig& B) { return A.Name < B.Name; });
|
||||
}
|
||||
|
||||
void UWingServer::OnModulesChanged(FName ModuleName, EModuleChangeReason Reason)
|
||||
{
|
||||
BuildWingHandlerRegistry();
|
||||
}
|
||||
|
||||
FWingHandlerConfig* UWingServer::FindHandler(const FString& Name)
|
||||
{
|
||||
int32 Index = Algo::LowerBoundBy(WingHandlerRegistry, Name, [](const FWingHandlerConfig& H) { return H.Name; });
|
||||
|
||||
@@ -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();
|
||||
@@ -325,7 +316,7 @@ WingVariables::Var WingVariables::LoadBlueprintVariableDescription(FBPVariableDe
|
||||
FProperty* Prop = CDO->GetClass()->FindPropertyByName(Desc.VarName);
|
||||
if (Prop)
|
||||
{
|
||||
Result.DefaultValue = FWingProperty(Prop, CDO, false).GetText();
|
||||
Result.DefaultValue = FWingProperty(CDO, CDO, Prop, false).GetText();
|
||||
Result.DefaultSpecified = true;
|
||||
}
|
||||
}
|
||||
@@ -500,7 +491,7 @@ bool WingVariables::ModifyBlueprintDefaults(WingOut Errors)
|
||||
*WingTokenizer::ExternalizeID(Input.Name));
|
||||
return false;
|
||||
}
|
||||
if (!FWingProperty(Prop, CDO, true).SetText(Input.DefaultValue, Errors)) return false;
|
||||
if (!FWingProperty(CDO, CDO, Prop, true).SetText(Input.DefaultValue, Errors)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
@@ -129,38 +115,6 @@ private:
|
||||
FStringBuilderBase *Buffer;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FWingStructAndUStruct.
|
||||
//
|
||||
// A pointer to a struct, and also a pointer to a UStruct
|
||||
// that describes the struct. This also can store a
|
||||
// UObject and the UClass that describes the UObject.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// A FWingStructAndUStruct is a pointer to a struct and its associated ustruct.
|
||||
//
|
||||
struct FWingStructAndUStruct
|
||||
{
|
||||
void *StructPtr;
|
||||
UStruct *UStructPtr;
|
||||
|
||||
// Explicit constructor.
|
||||
explicit FWingStructAndUStruct(void *Base, UStruct *S) : StructPtr(Base), UStructPtr(S) {}
|
||||
|
||||
// Copy constructor.
|
||||
FWingStructAndUStruct(FWingStructAndUStruct &Src) : StructPtr(Src.StructPtr), UStructPtr(Src.UStructPtr) {}
|
||||
|
||||
// Construct from a UObject.
|
||||
FWingStructAndUStruct(UObject *Obj) : StructPtr(Obj), UStructPtr(Obj->GetClass()) {}
|
||||
|
||||
// Construct from a UStruct pointer.
|
||||
template<class T, typename = std::enable_if_t<!std::is_base_of_v<UObject, T>>>
|
||||
FWingStructAndUStruct(T *Struct) : StructPtr(Struct), UStructPtr(Struct->StaticStruct()) {}
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// References.
|
||||
@@ -225,10 +179,11 @@ public:
|
||||
UPROPERTY()
|
||||
UObject* Object;
|
||||
|
||||
void *StructBase;
|
||||
|
||||
UPROPERTY()
|
||||
UStruct* StructType;
|
||||
|
||||
void *StructBase;
|
||||
|
||||
bool Editable;
|
||||
};
|
||||
|
||||
|
||||
@@ -48,4 +48,7 @@ public:
|
||||
|
||||
UFUNCTION()
|
||||
static void VariableGettersAndSetters();
|
||||
|
||||
UFUNCTION()
|
||||
static void BestPerformance();
|
||||
};
|
||||
|
||||
@@ -3,20 +3,24 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "WingBasics.h"
|
||||
|
||||
// A resolved property: the FProperty descriptor plus a pointer to
|
||||
// the value's storage.
|
||||
//
|
||||
struct FWingProperty
|
||||
{
|
||||
// To understand the following fields, imagine an object
|
||||
// that contains a struct, which contains another struct,
|
||||
// which contains another struct, which contains a field F.
|
||||
// In that case, Object points to the object that
|
||||
// contains everything, whereas Container points to the
|
||||
// innermost struct that contains the property.
|
||||
|
||||
FProperty* Prop = nullptr;
|
||||
TStrongObjectPtr<UObject> Object = nullptr;
|
||||
void* Container = nullptr;
|
||||
FProperty* Prop = nullptr;
|
||||
bool Editable = false;
|
||||
|
||||
// Construct a property reference.
|
||||
//
|
||||
FWingProperty(FProperty* InProp, void* InContainer, bool Edit)
|
||||
: Prop(InProp), Container(InContainer), Editable(Edit) {}
|
||||
FWingProperty(UObject *InObject, void* InContainer, FProperty* InProp, bool Edit)
|
||||
: Object(InObject), Container(InContainer), Prop(InProp), Editable(Edit) {}
|
||||
|
||||
// Construct a null property reference.
|
||||
//
|
||||
@@ -37,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
|
||||
@@ -72,14 +75,24 @@ struct FWingProperty
|
||||
//
|
||||
// This gets the properties that are literally present in the
|
||||
// specified object or struct. No special interpretation is done.
|
||||
// If mutable is false, all properties will be marked non-editable.
|
||||
//
|
||||
static TArray<FWingProperty> GetAll(FWingStructAndUStruct Obj);
|
||||
static TArray<FWingProperty> GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
|
||||
|
||||
// Get all the visible properties of the specified object or struct.
|
||||
//
|
||||
// This gets the properties that have CPF_Edit marked on them.
|
||||
// If mutable is false, all properties will be marked non-editable.
|
||||
//
|
||||
static TArray<FWingProperty> GetVisible(FWingStructAndUStruct Obj);
|
||||
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.
|
||||
//
|
||||
static TArray<FWingProperty> GetAll(UObject *Obj, bool Mutable)
|
||||
{ return GetAll(Obj, Obj, Obj->GetClass(), Mutable); }
|
||||
static TArray<FWingProperty> GetVisible(UObject *Obj, bool Mutable)
|
||||
{ return GetVisible(Obj, Obj, Obj->GetClass(), Mutable); }
|
||||
|
||||
// Get just the names of the properties of the specified struct/class.
|
||||
//
|
||||
@@ -108,18 +121,7 @@ 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 void StripEditable(TArray<FWingProperty> &Props);
|
||||
static bool IsUnsigned(FNumericProperty* Prop);
|
||||
static bool IsPinTypeProperty(FProperty *Prop);
|
||||
void PrintExpectsReceived(const TCHAR *Type, WingOut Errors) const;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -76,14 +73,14 @@ private:
|
||||
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
|
||||
TArray<FWingHandlerConfig> WingHandlerRegistry; // sorted by Name
|
||||
void BuildWingHandlerRegistry();
|
||||
void OnModulesChanged(FName ModuleName, EModuleChangeReason Reason);
|
||||
FDelegateHandle ModulesChangedHandle;
|
||||
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;
|
||||
@@ -100,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;
|
||||
|
||||
@@ -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,16 +75,6 @@ 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
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include "CoreMinimal.h"
|
||||
#include "CoreUObject.h"
|
||||
#include "Containers/Deque.h"
|
||||
|
||||
@@ -125,6 +125,21 @@ enum class ElxLuaSyntaxCheck : uint8 {
|
||||
InvalidLua,
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// ElxControllerType
|
||||
//
|
||||
// The three types of controller recognized by luprex.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ElxControllerType : uint8 {
|
||||
KeyboardMouse,
|
||||
XboxGamepad,
|
||||
PlayStationGamepad,
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Log Categories
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "Layout/Geometry.h"
|
||||
#include "Widgets/Layout/Anchors.h"
|
||||
#include "Common.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
#include "Slate/SGameLayerManager.h"
|
||||
#include "Kismet/KismetTextLibrary.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
@@ -246,17 +248,32 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataTransform(const FTra
|
||||
|
||||
FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataGeometry(const FGeometry &AutoConvertedValue, const FString &Name)
|
||||
{
|
||||
FVector2D LocalSize = AutoConvertedValue.GetLocalSize();
|
||||
FVector2D AbsPos = AutoConvertedValue.GetAbsolutePosition();
|
||||
FVector2D AbsSize = AutoConvertedValue.GetAbsoluteSize();
|
||||
FVector2D UL = AutoConvertedValue.GetAbsolutePosition();
|
||||
FVector2D LR = AutoConvertedValue.GetAbsolutePositionAtCoordinates(FVector2f(1.0f, 1.0f));
|
||||
if (GEngine && GEngine->GameViewport)
|
||||
{
|
||||
TSharedPtr<IGameLayerManager> GameLayerManager = GEngine->GameViewport->GetGameLayerManager();
|
||||
if (GameLayerManager.IsValid())
|
||||
{
|
||||
const FGeometry ViewportGeometry = GameLayerManager->GetViewportWidgetHostGeometry();
|
||||
const FVector2D ViewportLocalSize = FVector2D(ViewportGeometry.GetLocalSize());
|
||||
FVector2D ViewportPixelSize;
|
||||
GEngine->GameViewport->GetViewportSize(ViewportPixelSize);
|
||||
if (ViewportLocalSize.X > 0.0 && ViewportLocalSize.Y > 0.0)
|
||||
{
|
||||
const FVector2D PixelScale = ViewportPixelSize / ViewportLocalSize;
|
||||
UL = ViewportGeometry.AbsoluteToLocal(UL) * PixelScale;
|
||||
LR = ViewportGeometry.AbsoluteToLocal(LR) * PixelScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
FFormatArgumentData Result;
|
||||
Result.ArgumentValueType = EFormatArgumentType::Text;
|
||||
Result.ArgumentName = Name;
|
||||
Result.ArgumentValue = FText::Format(
|
||||
INVTEXT("Geom(Local={0}x{1} Abs={2}x{3} Pos={4},{5})"),
|
||||
FText::AsNumber(LocalSize.X), FText::AsNumber(LocalSize.Y),
|
||||
FText::AsNumber(AbsSize.X), FText::AsNumber(AbsSize.Y),
|
||||
FText::AsNumber(AbsPos.X), FText::AsNumber(AbsPos.Y));
|
||||
INVTEXT("UL={0},{1} LR={2},{3}"),
|
||||
FText::AsNumber(UL.X), FText::AsNumber(UL.Y),
|
||||
FText::AsNumber(LR.X), FText::AsNumber(LR.Y));
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -300,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
|
||||
@@ -312,7 +335,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
|
||||
double Now = FPlatformTime::Seconds();
|
||||
FString Key = Context->GetClass()->GetName() + TEXT("::") + InPattern;
|
||||
double &Last = LastLogTime.FindOrAdd(Key, 0.0);
|
||||
if (Now - Last < 1.0)
|
||||
if (Now - Last < 2.0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -321,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.
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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.
|
||||
|
||||
105
Source/Integration/InputDeviceTracker.cpp
Normal file
105
Source/Integration/InputDeviceTracker.cpp
Normal 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;
|
||||
}
|
||||
64
Source/Integration/InputDeviceTracker.h
Normal file
64
Source/Integration/InputDeviceTracker.h
Normal 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;
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InputModeRequest.cpp
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#include "InputModeRequest.h"
|
||||
#include "Common.h"
|
||||
|
||||
bool FlxInputModeRequest::operator<(const FlxInputModeRequest &Other) const
|
||||
{
|
||||
// The highest priority request goes to the front of the array.
|
||||
// Therefore, in this context, 'less than' means 'higher priority'.
|
||||
// It's a little confusing.
|
||||
if (ShowPointer != Other.ShowPointer) return ShowPointer > Other.ShowPointer;
|
||||
return SequenceNumber > Other.SequenceNumber;
|
||||
}
|
||||
|
||||
bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const
|
||||
{
|
||||
return (Widget == Other.Widget) &&
|
||||
(Focus == Other.Focus) &&
|
||||
(ShowPointer == Other.ShowPointer) &&
|
||||
(BlockInput == Other.BlockInput) &&
|
||||
(EnableInputComponent == Other.EnableInputComponent);
|
||||
}
|
||||
|
||||
bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request)
|
||||
{
|
||||
if (Request.Widget == nullptr)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
int32 FlxInputModeRequests::FindWidget(UUserWidget *Widget)
|
||||
{
|
||||
for (const FlxInputModeRequest &Req : Requests)
|
||||
{
|
||||
if (Req.Widget == Widget) return &Req - Requests.GetData();
|
||||
}
|
||||
return Requests.Num();
|
||||
}
|
||||
|
||||
void FlxInputModeRequests::BubbleItem(int32 Index)
|
||||
{
|
||||
while ((Index > 0) && (Requests[Index] < Requests[Index - 1]))
|
||||
{
|
||||
Swap(Requests[Index], Requests[Index - 1]);
|
||||
--Index;
|
||||
}
|
||||
while ((Index < Requests.Num() - 1) && (Requests[Index + 1] < Requests[Index]))
|
||||
{
|
||||
Swap(Requests[Index], Requests[Index + 1]);
|
||||
++Index;
|
||||
}
|
||||
}
|
||||
|
||||
void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence)
|
||||
{
|
||||
int32 Index = FindWidget(NewRequest.Widget);
|
||||
|
||||
if (Index == Requests.Num())
|
||||
{
|
||||
Requests.Emplace(NewRequest);
|
||||
Requests[Index].SequenceNumber = ++NextSequenceNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
int32 SequenceNumber = Requests[Index].SequenceNumber;
|
||||
if (UpdateSequence) SequenceNumber = ++NextSequenceNumber;
|
||||
Requests[Index] = NewRequest;
|
||||
Requests[Index].SequenceNumber = SequenceNumber;
|
||||
}
|
||||
|
||||
BubbleItem(Index);
|
||||
}
|
||||
|
||||
void FlxInputModeRequests::SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent)
|
||||
{
|
||||
int32 Index = FindWidget(Widget);
|
||||
|
||||
if (Index == Requests.Num())
|
||||
{
|
||||
FlxInputModeRequest NewReq;
|
||||
NewReq.Widget = Widget;
|
||||
NewReq.EnableInputComponent = EnableInputComponent;
|
||||
NewReq.SequenceNumber = ++NextSequenceNumber;
|
||||
Requests.Emplace(NewReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
Requests[Index].EnableInputComponent = EnableInputComponent;
|
||||
}
|
||||
|
||||
BubbleItem(Index);
|
||||
}
|
||||
|
||||
void FlxInputModeRequests::Remove(UUserWidget *Widget)
|
||||
{
|
||||
Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry)
|
||||
{
|
||||
return Entry.Widget == Widget;
|
||||
});
|
||||
}
|
||||
|
||||
void FlxInputModeRequests::GarbageCollect()
|
||||
{
|
||||
Requests.RemoveAll([](const FlxInputModeRequest &Entry)
|
||||
{
|
||||
return !IsValid(Entry.Widget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InputModeRequest.h
|
||||
//
|
||||
// Custom input event dispatching system. Uses Unreal's
|
||||
// built-in input modes (GameOnly / UIOnly) with an
|
||||
// enhanced input component for GameOnly mode hotkeys.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InputModeRequest.generated.h"
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FlxInputModeRequest
|
||||
//
|
||||
// Using this struct, a Widget can express a need for a
|
||||
// particular input mode. These requests go to the player
|
||||
// controller, which arbitrates.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FlxInputModeRequest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FlxInputModeRequest() = default;
|
||||
|
||||
bool operator == (const FlxInputModeRequest &Other) const;
|
||||
bool operator < (const FlxInputModeRequest &Other) const;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
UUserWidget* Widget = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
UWidget* Focus = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
bool ShowPointer = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
bool BlockInput = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
bool EnableInputComponent = true;
|
||||
|
||||
UPROPERTY()
|
||||
int32 SequenceNumber = 0;
|
||||
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
struct FlxInputModeRequests
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
// Sorted by highest priority first, then most recent first.
|
||||
TArray<FlxInputModeRequest> Requests;
|
||||
|
||||
UPROPERTY()
|
||||
int32 NextSequenceNumber = 0;
|
||||
|
||||
public:
|
||||
// Get the requests array.
|
||||
const TArray<FlxInputModeRequest> &GetRequests() const { return Requests; }
|
||||
|
||||
// Sanity check a request to see if it is reasonable.
|
||||
static bool SanityCheck(const FlxInputModeRequest &Request);
|
||||
|
||||
// Apply a request. Replaces any previous request by the same widget.
|
||||
void Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence = true);
|
||||
|
||||
// Find the specified widget, and modify the 'EnableInputComponent'
|
||||
// flag. Adds the widget if it's not already present.
|
||||
void SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent);
|
||||
|
||||
// Remove all requests by the specified widget.
|
||||
void Remove(UUserWidget *Widget);
|
||||
|
||||
// Remove any requests by dead widgets or widgets with no parents.
|
||||
void GarbageCollect();
|
||||
|
||||
private:
|
||||
// Find specified widget. If not present, returns Requests.Num()
|
||||
int32 FindWidget(UUserWidget *Widget);
|
||||
|
||||
// Move item at Index to its proper place in the array by priority.
|
||||
void BubbleItem(int32 Index);
|
||||
};
|
||||
@@ -31,6 +31,7 @@ public class Integration : ModuleRules
|
||||
"UMG",
|
||||
"UMGEditor",
|
||||
"EditorSubsystem",
|
||||
"ApplicationCore",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.Add("Slate");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,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")
|
||||
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
#include "LuprexViewportClient.h"
|
||||
#include "Common.h"
|
||||
#include "PlayerControllerBase.h"
|
||||
#include "RootCanvas.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Layout/WidgetPath.h"
|
||||
#include "Widgets/SViewport.h"
|
||||
#include "Slate/SObjectWidget.h"
|
||||
|
||||
UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
@@ -18,44 +19,44 @@ UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer
|
||||
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient constructed"));
|
||||
}
|
||||
|
||||
bool UlxViewportClient::TryBringToFront(const FWidgetPath &Path)
|
||||
{
|
||||
UGameInstance *GI = GetGameInstance();
|
||||
if (!GI) return false;
|
||||
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(
|
||||
GI->GetFirstLocalPlayerController(GetWorld()));
|
||||
if (!PC) return false;
|
||||
|
||||
for (int32 Idx = 0; Idx < Path.Widgets.Num(); ++Idx)
|
||||
{
|
||||
SWidget &SW = Path.Widgets[Idx].Widget.Get();
|
||||
if (SW.GetType() != FName(TEXT("SObjectWidget"))) continue;
|
||||
UUserWidget *Widget = static_cast<SObjectWidget&>(SW).GetWidgetObject();
|
||||
if (Widget && Widget->GetParent() == PC->RootCanvas)
|
||||
{
|
||||
UlxRootCanvasPanel::BringToFront(Widget);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UlxViewportClient::InputKey(const FInputKeyEventArgs &EventArgs)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient::InputKey key=%s event=%d"),
|
||||
*EventArgs.Key.ToString(), (int32)EventArgs.Event);
|
||||
|
||||
// Only act on left mouse button presses that bubbled up to the
|
||||
// viewport unhandled. Walk the widget path under the cursor and
|
||||
// find the nearest focusable ancestor of whatever was hit. If it
|
||||
// isn't the game viewport itself, hand it to the player controller
|
||||
// to apply on its next UpdateInputMode pass; that's the point in
|
||||
// the frame where we can override SViewport's own click-focus
|
||||
// behaviour without fighting it.
|
||||
// viewport unhandled. If the click landed on a descendant of a
|
||||
// top-level widget in the root canvas, bring that top-level widget
|
||||
// to the front.
|
||||
if ((EventArgs.Event == IE_Pressed) && (EventArgs.Key == EKeys::LeftMouseButton))
|
||||
{
|
||||
FSlateApplication &Slate = FSlateApplication::Get();
|
||||
FVector2D MousePos = Slate.GetCursorPos();
|
||||
FWidgetPath Path = Slate.LocateWindowUnderMouse(
|
||||
MousePos, Slate.GetInteractiveTopLevelWindows());
|
||||
|
||||
if (Path.IsValid())
|
||||
{
|
||||
TSharedPtr<SViewport> ViewportWidget = GetGameViewportWidget();
|
||||
for (int32 Idx = Path.Widgets.Num() - 1; Idx >= 0; --Idx)
|
||||
{
|
||||
TSharedRef<SWidget> Widget = Path.Widgets[Idx].Widget;
|
||||
if (!Widget->SupportsKeyboardFocus()) continue;
|
||||
if (ViewportWidget.IsValid() && Widget == ViewportWidget) break;
|
||||
if (UGameInstance *GI = GetGameInstance())
|
||||
{
|
||||
if (AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(
|
||||
GI->GetFirstLocalPlayerController(GetWorld())))
|
||||
{
|
||||
PC->ClickToFocus(Widget);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Path.IsValid() && TryBringToFront(Path)) return true;
|
||||
}
|
||||
|
||||
return Super::InputKey(EventArgs);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
#include "Layout/WidgetPath.h"
|
||||
#include "LuprexViewportClient.generated.h"
|
||||
|
||||
UCLASS()
|
||||
@@ -25,4 +26,7 @@ public:
|
||||
UlxViewportClient(const FObjectInitializer &ObjectInitializer);
|
||||
|
||||
virtual bool InputKey(const FInputKeyEventArgs &EventArgs) override;
|
||||
|
||||
private:
|
||||
bool TryBringToFront(const FWidgetPath &Path);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "PlayerControllerBase.h"
|
||||
#include "Common.h"
|
||||
#include "GameFramework/InputDeviceSubsystem.h"
|
||||
#include "GameFramework/InputSettings.h"
|
||||
#include "RootCanvas.h"
|
||||
#include "Tangible.h"
|
||||
#include "TangibleManager.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "Components/InputComponent.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
@@ -63,7 +67,17 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
|
||||
|
||||
void AlxPlayerControllerBase::BeginPlay()
|
||||
{
|
||||
// Build the root UMG stack BEFORE Super::BeginPlay. Super calls
|
||||
// ReceiveBeginPlay, which fires the Blueprint Event BeginPlay;
|
||||
// BP code there may immediately try to add widgets to RootCanvas,
|
||||
// so the canvas must already exist.
|
||||
RootWidget = CreateWidget<UlxRootWidget>(this);
|
||||
RootCanvas = RootWidget->WidgetTree->ConstructWidget<UlxRootCanvasPanel>();
|
||||
RootWidget->WidgetTree->RootWidget = RootCanvas;
|
||||
RootWidget->AddToViewport(0);
|
||||
|
||||
Super::BeginPlay();
|
||||
|
||||
if (FSlateApplication::IsInitialized())
|
||||
{
|
||||
FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject(
|
||||
@@ -78,6 +92,14 @@ void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle);
|
||||
FocusChangingHandle.Reset();
|
||||
}
|
||||
|
||||
if (IsValid(RootWidget))
|
||||
{
|
||||
RootWidget->RemoveFromParent();
|
||||
}
|
||||
RootWidget = nullptr;
|
||||
RootCanvas = nullptr;
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
@@ -111,11 +133,11 @@ UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *W
|
||||
return Cast<UInputComponent>(Value);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus, bool EnableInputComponent)
|
||||
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
|
||||
{
|
||||
if (!IsValid(Widget))
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("WidgetRequestInputMode called with an invalid widget."));
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("AddWidgetToRoot called with an invalid widget."));
|
||||
return;
|
||||
}
|
||||
APlayerController *OwningPC = Widget->GetOwningPlayer();
|
||||
@@ -123,53 +145,20 @@ void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool S
|
||||
if (PC == nullptr)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error,
|
||||
TEXT("WidgetRequestInputMode: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
|
||||
TEXT("AddWidgetToRoot: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
|
||||
*Widget->GetName(), *GetNameSafe(OwningPC));
|
||||
return;
|
||||
}
|
||||
FlxInputModeRequest Req;
|
||||
Req.Widget = Widget;
|
||||
Req.Focus = Focus;
|
||||
Req.ShowPointer = ShowPointer;
|
||||
Req.BlockInput = BlockInput;
|
||||
Req.EnableInputComponent = EnableInputComponent;
|
||||
PC->InputModeRequests.Request(Req);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
|
||||
{
|
||||
if (InInputComponent)
|
||||
if (PC->RootCanvas == nullptr)
|
||||
{
|
||||
// Widgets don't go on the current input stack. Instead, they're
|
||||
// tracked in InputModeRequests, where the PlayerController can arbitrate
|
||||
// focus, pointer visibility, and input blocking across them.
|
||||
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
|
||||
{
|
||||
InputModeRequests.SetEnableInputComponent(Widget, true);
|
||||
return;
|
||||
}
|
||||
CurrentInputStack.RemoveSingle(InInputComponent);
|
||||
CurrentInputStack.Add(InInputComponent);
|
||||
UE_LOG(LogLuprexIntegration, Error,
|
||||
TEXT("AddWidgetToRoot: root canvas is not initialized, this is probably an initialization order issue"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool AlxPlayerControllerBase::PopInputComponent(UInputComponent* InInputComponent)
|
||||
{
|
||||
if (InInputComponent)
|
||||
{
|
||||
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
|
||||
{
|
||||
InputModeRequests.SetEnableInputComponent(Widget, false);
|
||||
InInputComponent->ClearBindingValues();
|
||||
return true;
|
||||
}
|
||||
if (CurrentInputStack.RemoveSingle(InInputComponent) > 0)
|
||||
{
|
||||
InInputComponent->ClearBindingValues();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (Widget->GetParent() == PC->RootCanvas) return;
|
||||
|
||||
PC->RootCanvas->AddChildToRootCanvas(Widget);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
|
||||
@@ -216,51 +205,51 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
|
||||
InputStack.Push(InputComponent);
|
||||
}
|
||||
|
||||
// The current input stack is unlikely to have anything,
|
||||
// given that we've moved widgets to their own mechanism.
|
||||
// But, if there's anything here, sort it and push it.
|
||||
if (!CurrentInputStack.IsEmpty())
|
||||
// Get the widget slots.
|
||||
TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
|
||||
|
||||
// Generate a set of input components that are being managed by the WidgetSlots.
|
||||
TSet<UInputComponent*> WidgetManagedComponents;
|
||||
for (UlxRootCanvasSlot *Slot : WidgetSlots)
|
||||
{
|
||||
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
|
||||
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
|
||||
{
|
||||
UInputComponent* IC = CurrentInputStack[Idx].Get();
|
||||
if (IsValid(IC)) Pushed.Add(IC);
|
||||
else CurrentInputStack.RemoveAt(Idx--);
|
||||
}
|
||||
|
||||
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
|
||||
{
|
||||
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
|
||||
return A.Priority < B.Priority;
|
||||
});
|
||||
|
||||
InputStack.Append(Pushed);
|
||||
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
|
||||
UInputComponent *IC = GetWidgetInputComponent(Widget);
|
||||
if (IC) WidgetManagedComponents.Add(IC);
|
||||
}
|
||||
|
||||
// Push the widget input components from InputModeRequests. Requests
|
||||
// are ordered high-priority first; the input stack is processed
|
||||
// from top (last index) down, so we push back-to-front: low
|
||||
// priority first, high priority last (ending up on top).
|
||||
const TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
|
||||
for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx)
|
||||
// Add components in the CurrentInputStack, *unless* they are being managed
|
||||
// by widgets. If they're being managed by widgets, they get added later.
|
||||
for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx)
|
||||
{
|
||||
const FlxInputModeRequest &Req = Requests[Idx];
|
||||
if (!Req.EnableInputComponent) continue;
|
||||
UUserWidget *Widget = Req.Widget;
|
||||
if (!Widget->IsInViewport()) continue;
|
||||
if (UInputComponent *IC = GetWidgetInputComponent(Widget))
|
||||
UInputComponent* IC = CurrentInputStack[Idx].Get();
|
||||
if (IsValid(IC))
|
||||
{
|
||||
IC->bBlockInput = Req.BlockInput;
|
||||
InputStack.Push(IC);
|
||||
if (!WidgetManagedComponents.Contains(IC)) InputStack.Push(IC);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentInputStack.RemoveAt(Idx--);
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the widget-managed input components.
|
||||
for (UlxRootCanvasSlot *Slot : WidgetSlots)
|
||||
{
|
||||
if (Slot->EnableEnhancedInput)
|
||||
{
|
||||
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
|
||||
UInputComponent *IC = GetWidgetInputComponent(Widget);
|
||||
if (IC)
|
||||
{
|
||||
IC->bBlockInput = Slot->BlockInput;
|
||||
InputStack.Push(IC);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::UpdateInputMode()
|
||||
{
|
||||
InputModeRequests.GarbageCollect();
|
||||
|
||||
// Get all the various objects we need to be able to manipulate
|
||||
// the input mode.
|
||||
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
|
||||
@@ -273,19 +262,23 @@ void AlxPlayerControllerBase::UpdateInputMode()
|
||||
TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser();
|
||||
if (!SlateUser.IsValid()) return;
|
||||
|
||||
// The first active entry in InputModeRequests dictates the
|
||||
// pointer / capture / focus state. If there are no requests at all,
|
||||
// fall back to a static default-constructed request.
|
||||
static const FlxInputModeRequest EmptyRequest;
|
||||
const FlxInputModeRequest *Top = &EmptyRequest;
|
||||
for (const FlxInputModeRequest &Req : InputModeRequests.GetRequests())
|
||||
|
||||
RootCanvas->UpdateZOrders();
|
||||
|
||||
// Get the desired configuration from the top widget.
|
||||
UUserWidget *Widget = nullptr;
|
||||
UWidget *Focus = nullptr;
|
||||
bool ShowPointer = false;
|
||||
if (UlxRootCanvasSlot *Top = RootCanvas->GetTopWidget())
|
||||
{
|
||||
if (Req.Widget->IsInViewport()) { Top = &Req; break; }
|
||||
Widget = Cast<UUserWidget>(Top->GetContent());
|
||||
Focus = Widget->GetDesiredFocusWidget();
|
||||
ShowPointer = Top->ShowPointer;
|
||||
}
|
||||
|
||||
SetShowMouseCursor(Top->ShowPointer);
|
||||
SetShowMouseCursor(ShowPointer);
|
||||
|
||||
if (Top->ShowPointer)
|
||||
if (ShowPointer)
|
||||
{
|
||||
// Only release capture if the viewport is currently holding it
|
||||
// (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture
|
||||
@@ -311,42 +304,20 @@ void AlxPlayerControllerBase::UpdateInputMode()
|
||||
|
||||
GameViewportClient->SetIgnoreInput(false);
|
||||
|
||||
// How we handle focus depends on whether we're showing the pointer.
|
||||
// In pointer mode, we set focus to the desired state just once,
|
||||
// and then we let the pointer control it from there on. In
|
||||
// no-pointer mode, we set focus to the desired state and
|
||||
// keep putting it back forever.
|
||||
//
|
||||
// If the user clicks the mouse on a focusable widget, the
|
||||
// viewport client notifies us of that fact. We then focus the
|
||||
// widget if possible.
|
||||
//
|
||||
if ((!Top->ShowPointer) || (LastRequestGrantedFocus != Top->SequenceNumber))
|
||||
// We always put keyboard focus on whatever user widget is in
|
||||
// front. If the front widget doesn't want keyboard focus,
|
||||
// then we put keyboard focus on the viewport.
|
||||
if (Focus)
|
||||
{
|
||||
if (Top->Focus)
|
||||
if (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
|
||||
{
|
||||
if (TSharedPtr<SWidget> SlateFocus = Top->Focus->GetCachedWidget())
|
||||
{
|
||||
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
|
||||
LastRequestGrantedFocus = Top->SequenceNumber;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SlateOperations.SetUserFocus(ViewportWidgetRef);
|
||||
LastRequestGrantedFocus = Top->SequenceNumber;
|
||||
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
|
||||
}
|
||||
}
|
||||
else if (TSharedPtr<SWidget> ClickedWidget = ClickToFocusTarget.Pin())
|
||||
else
|
||||
{
|
||||
SlateOperations.SetUserFocus(ClickedWidget.ToSharedRef());
|
||||
SlateOperations.SetUserFocus(ViewportWidgetRef);
|
||||
}
|
||||
ClickToFocusTarget.Reset();
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::ClickToFocus(TSharedRef<SWidget> Widget)
|
||||
{
|
||||
ClickToFocusTarget = Widget.ToWeakPtr();
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::UpdateLookAt()
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Common.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputModeRequest.h"
|
||||
#include "UObject/ObjectKey.h"
|
||||
#include "PlayerControllerBase.generated.h"
|
||||
|
||||
class UlxRootCanvasPanel;
|
||||
class UWidget;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
|
||||
{
|
||||
@@ -55,9 +59,6 @@ private:
|
||||
|
||||
public:
|
||||
|
||||
// Input stack overrides: unsorted, append-on-push.
|
||||
virtual void PushInputComponent(UInputComponent* InInputComponent) override;
|
||||
virtual bool PopInputComponent(UInputComponent* InInputComponent) override;
|
||||
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
|
||||
|
||||
// Read UUserWidget::InputComponent via reflection. The field is
|
||||
@@ -65,12 +66,10 @@ public:
|
||||
// FProperty so we always see the current value without caching it.
|
||||
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
|
||||
|
||||
// Blueprint-facing entry point. Looks like a method on UUserWidget
|
||||
// (thanks to DefaultToSelf + HideSelfPin): the widget self-binds,
|
||||
// we find its owning PlayerController, and register the request.
|
||||
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode",
|
||||
meta = (DefaultToSelf = "Widget", HideSelfPin = "true", EnableInputComponent = "true"))
|
||||
static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus, bool EnableInputComponent);
|
||||
// Add a widget to the root canvas at ZOrder 0 with default slot flags.
|
||||
UFUNCTION(BlueprintCallable, Category = "Luprex|Root Canvas",
|
||||
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
|
||||
static void AddWidgetToRoot(class UUserWidget *Widget);
|
||||
|
||||
// Get the player controller, cast to AlxPlayerControllerBase.
|
||||
static AlxPlayerControllerBase *FromContext(const UObject *Context);
|
||||
@@ -78,22 +77,16 @@ public:
|
||||
UPROPERTY()
|
||||
FHitResult CurrentLookAt;
|
||||
|
||||
// Input mode requests - see InputModeRequest.h for an explanation.
|
||||
// The single top-level UUserWidget added to the viewport. All
|
||||
// top-level UI widgets are children of RootCanvas inside it.
|
||||
UPROPERTY()
|
||||
FlxInputModeRequests InputModeRequests;
|
||||
UUserWidget *RootWidget = nullptr;
|
||||
|
||||
// The last input mode request whose focus request was granted.
|
||||
// The root canvas panel inside RootWidget. Children of this
|
||||
// canvas are the top-level widgets; their slots carry both
|
||||
// layout and input-mode configuration.
|
||||
UPROPERTY()
|
||||
int32 LastRequestGrantedFocus = 0;
|
||||
|
||||
// The viewport client uses this to notify us that the user
|
||||
// clicked on a focusable widget.
|
||||
void ClickToFocus(TSharedRef<SWidget> Widget);
|
||||
|
||||
private:
|
||||
TWeakPtr<SWidget> ClickToFocusTarget;
|
||||
|
||||
public:
|
||||
UlxRootCanvasPanel *RootCanvas = nullptr;
|
||||
|
||||
bool MustCallLookAtChanged = false;
|
||||
};
|
||||
|
||||
251
Source/Integration/PromptWidget.cpp
Normal file
251
Source/Integration/PromptWidget.cpp
Normal file
@@ -0,0 +1,251 @@
|
||||
#include "PromptWidget.h"
|
||||
#include "InputDeviceTracker.h"
|
||||
#include "UtilityLibrary.h"
|
||||
#include "PlayerControllerBase.h"
|
||||
#include "InputAction.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Widgets/Layout/SScaleBox.h"
|
||||
|
||||
//
|
||||
// Atlas contents:
|
||||
//
|
||||
// Row 1: Keyboard Button no Glyph, Left Mouse, Middle Mouse, Right Mouse
|
||||
// Row 2: Left Trigger, Right Trigger, Left Shoulder, Right Shoulder
|
||||
// Row 3: XBFace Down, XBFace Left, XBFace Up, XBFace Right
|
||||
// Row 4: PSFace Down, PSFace Left, PSFace Up, PSFace Right
|
||||
// Row 5: Arrow Down, Arrow Left, Arrow Up, Arrow Right
|
||||
// Row 6: DPad Down, DPad Left, DPad Up, DPad Right
|
||||
// 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)
|
||||
if (Key == EKeys::LeftMouseButton) return 1;
|
||||
if (Key == EKeys::MiddleMouseButton) return 2;
|
||||
if (Key == EKeys::RightMouseButton) return 3;
|
||||
|
||||
// Gamepad triggers and shoulders (row 2)
|
||||
if (Key == EKeys::Gamepad_LeftTrigger) return 4;
|
||||
if (Key == EKeys::Gamepad_RightTrigger) return 5;
|
||||
if (Key == EKeys::Gamepad_LeftShoulder) return 6;
|
||||
if (Key == EKeys::Gamepad_RightShoulder) return 7;
|
||||
|
||||
// Gamepad Face buttons (rows 3 and 4)
|
||||
if (Key == EKeys::Gamepad_FaceButton_Bottom) return Playstation ? 12 : 8;
|
||||
if (Key == EKeys::Gamepad_FaceButton_Left) return Playstation ? 13 : 9;
|
||||
if (Key == EKeys::Gamepad_FaceButton_Top) return Playstation ? 14 : 10;
|
||||
if (Key == EKeys::Gamepad_FaceButton_Right) return Playstation ? 15 : 11;
|
||||
|
||||
// Arrow keys (row 5)
|
||||
if (Key == EKeys::Down) return 16;
|
||||
if (Key == EKeys::Left) return 17;
|
||||
if (Key == EKeys::Up) return 18;
|
||||
if (Key == EKeys::Right) return 19;
|
||||
|
||||
// Gamepad D-Pad (row 6)
|
||||
if (Key == EKeys::Gamepad_DPad_Down) return 20;
|
||||
if (Key == EKeys::Gamepad_DPad_Left) return 21;
|
||||
if (Key == EKeys::Gamepad_DPad_Up) return 22;
|
||||
if (Key == EKeys::Gamepad_DPad_Right) return 23;
|
||||
|
||||
// Space bar (row 7)
|
||||
if (Key == EKeys::SpaceBar) return 24;
|
||||
|
||||
// No icon for this key. Use the blank icon with a label.
|
||||
return 0;
|
||||
}
|
||||
|
||||
void UlxPromptWidget::ChooseAppearance(int32 &OutIcon, FString &OutGlyph)
|
||||
{
|
||||
FKey Key = (ControllerType == ElxControllerType::KeyboardMouse) ? KeyboardKey : GamepadKey;
|
||||
OutIcon = ChooseIcon(ControllerType == ElxControllerType::PlayStationGamepad, Key);
|
||||
if (OutIcon == 0) OutGlyph = Key.GetDisplayName().ToString();
|
||||
else OutGlyph = TEXT("");
|
||||
}
|
||||
|
||||
|
||||
FBox2f UlxPromptWidget::GetIconUVs(int32 IconIndex)
|
||||
{
|
||||
const float ColWidth = 1.0f / 4.0f;
|
||||
const float RowHeight = 1.0f / 8.0f;
|
||||
int32 Col = IconIndex % 4;
|
||||
int32 Row = IconIndex / 4;
|
||||
float U = Col * ColWidth;
|
||||
float V = Row * RowHeight;
|
||||
return FBox2f(FVector2f(U, V), FVector2f(U + ColWidth, V + RowHeight));
|
||||
}
|
||||
|
||||
FMargin UlxPromptWidget::GetScaledMargins() const
|
||||
{
|
||||
return FMargin(
|
||||
GlyphMargins.Left * Size.X,
|
||||
GlyphMargins.Top * Size.Y,
|
||||
GlyphMargins.Right * Size.X,
|
||||
GlyphMargins.Bottom * Size.Y);
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
if (!MyImage.IsValid()) return;
|
||||
|
||||
int32 Icon = 0;
|
||||
FString Glyph;
|
||||
ChooseAppearance(Icon, Glyph);
|
||||
|
||||
MyBrush.SetUVRegion(GetIconUVs(Icon));
|
||||
MyImage->InvalidateImage();
|
||||
MyBox->SetWidthOverride(Size.X);
|
||||
MyBox->SetHeightOverride(Size.Y);
|
||||
const float Extra = Depressed ? 0.05f : 0.0f;
|
||||
const FMargin ExtraPadding(Extra * Size.X, Extra * Size.Y, Extra * Size.X, Extra * Size.Y);
|
||||
MyImageSlot->SetPadding(ExtraPadding);
|
||||
MyGlyphSlot->SetPadding(GetScaledMargins() + ExtraPadding);
|
||||
MyGlyph->SetColorAndOpacity(GlyphColor);
|
||||
|
||||
if (!Glyph.IsEmpty())
|
||||
{
|
||||
MyGlyph->SetText(FText::FromString(Glyph));
|
||||
MyGlyph->SetVisibility(EVisibility::HitTestInvisible);
|
||||
}
|
||||
else
|
||||
{
|
||||
MyGlyph->SetVisibility(EVisibility::Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetSize(FVector2D InSize)
|
||||
{
|
||||
if (Size != InSize)
|
||||
{
|
||||
Size = InSize;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetGlyphMargins(FMargin InMargins)
|
||||
{
|
||||
if (GlyphMargins != InMargins)
|
||||
{
|
||||
GlyphMargins = InMargins;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetGlyphColor(FLinearColor InColor)
|
||||
{
|
||||
if (GlyphColor != InColor)
|
||||
{
|
||||
GlyphColor = InColor;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetDepressed(bool InDepressed)
|
||||
{
|
||||
if (Depressed != InDepressed)
|
||||
{
|
||||
Depressed = InDepressed;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetKeys(FKey InGamepadKey, FKey InKeyboardKey)
|
||||
{
|
||||
if ((GamepadKey != InGamepadKey) || (KeyboardKey != InKeyboardKey))
|
||||
{
|
||||
GamepadKey = InGamepadKey;
|
||||
KeyboardKey = InKeyboardKey;
|
||||
SynchronizeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
void UlxPromptWidget::SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction)
|
||||
{
|
||||
check(InputMappingContext);
|
||||
check(EnhancedInputAction);
|
||||
|
||||
FKey NewFirstKey;
|
||||
FKey NewGamepadKey;
|
||||
FKey NewKeyboardKey;
|
||||
|
||||
for (const FEnhancedActionKeyMapping& Mapping : InputMappingContext->GetMappings())
|
||||
{
|
||||
if (Mapping.Action != EnhancedInputAction) continue;
|
||||
FKey Key = Mapping.Key;
|
||||
if (Key.IsDigital())
|
||||
{
|
||||
if (!NewFirstKey.IsValid()) NewFirstKey = Mapping.Key;
|
||||
if (Key.IsTouch()) { /* not supported */ }
|
||||
else if (Key.IsGesture()) { /* not supported */ }
|
||||
else if (Key.IsGamepadKey()) { if (!NewGamepadKey.IsValid()) NewGamepadKey = Key; }
|
||||
else { if (!NewKeyboardKey.IsValid()) NewKeyboardKey = Key; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!NewGamepadKey.IsValid()) NewGamepadKey = NewFirstKey;
|
||||
if (!NewKeyboardKey.IsValid()) NewKeyboardKey = NewFirstKey;
|
||||
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 = UlxInputDeviceTracker::GetLastControllerType();
|
||||
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UlxPromptWidget::OnTick));
|
||||
}
|
||||
|
||||
MyBrush.SetResourceObject(ButtonAtlas.Get());
|
||||
|
||||
SAssignNew(MyImage, SImage).Image(&MyBrush);
|
||||
|
||||
SAssignNew(MyGlyph, STextBlock);
|
||||
SAssignNew(MyScaleBox, SScaleBox)
|
||||
.Stretch(EStretch::ScaleToFit)
|
||||
[ MyGlyph.ToSharedRef() ];
|
||||
|
||||
MyOverlay = SNew(SOverlay);
|
||||
MyOverlay->AddSlot().Expose(MyImageSlot)
|
||||
[ MyImage.ToSharedRef() ];
|
||||
MyOverlay->AddSlot().HAlign(HAlign_Fill).VAlign(VAlign_Fill).Expose(MyGlyphSlot)
|
||||
[ MyScaleBox.ToSharedRef() ];
|
||||
|
||||
SAssignNew(MyBox, SBox)
|
||||
[ MyOverlay.ToSharedRef() ];
|
||||
|
||||
SynchronizeProperties();
|
||||
return MyBox.ToSharedRef();
|
||||
}
|
||||
|
||||
void UlxPromptWidget::ReleaseSlateResources(bool bReleaseChildren)
|
||||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
if (!IsDesignTime())
|
||||
{
|
||||
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
||||
}
|
||||
MyBox.Reset();
|
||||
MyOverlay.Reset();
|
||||
MyImage.Reset();
|
||||
MyScaleBox.Reset();
|
||||
MyGlyph.Reset();
|
||||
MyImageSlot = nullptr;
|
||||
MyGlyphSlot = nullptr;
|
||||
}
|
||||
89
Source/Integration/PromptWidget.h
Normal file
89
Source/Integration/PromptWidget.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Common.h"
|
||||
#include "Components/Widget.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "Widgets/Layout/SBox.h"
|
||||
#include "Widgets/Layout/SScaleBox.h"
|
||||
#include "Containers/Ticker.h"
|
||||
#include "PromptWidget.generated.h"
|
||||
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class INTEGRATION_API UlxPromptWidget : public UWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||
TObjectPtr<UTexture2D> ButtonAtlas;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||
ElxControllerType ControllerType = ElxControllerType::KeyboardMouse;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||
FKey GamepadKey = EKeys::Gamepad_FaceButton_Left;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||
FKey KeyboardKey = EKeys::Z;
|
||||
|
||||
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||
FVector2D Size = FVector2D(64, 64);
|
||||
|
||||
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||
FMargin GlyphMargins = FMargin(0.0f, 0.1f, 0.0f, 0.1f);
|
||||
|
||||
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||
FLinearColor GlyphColor = FLinearColor::White;
|
||||
|
||||
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||
bool Depressed = false;
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetKeys(FKey InGamepadKey, FKey InKeyboardKey);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetGlyphMargins(FMargin InMargins);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetGlyphColor(FLinearColor InColor);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetSize(FVector2D InSize);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||
void SetDepressed(bool InDepressed);
|
||||
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
|
||||
private:
|
||||
FSlateBrush MyBrush;
|
||||
TSharedPtr<SBox> MyBox;
|
||||
TSharedPtr<SOverlay> MyOverlay;
|
||||
TSharedPtr<SImage> MyImage;
|
||||
TSharedPtr<SScaleBox> MyScaleBox;
|
||||
TSharedPtr<STextBlock> MyGlyph;
|
||||
SOverlay::FOverlaySlot* MyImageSlot = nullptr;
|
||||
SOverlay::FOverlaySlot* MyGlyphSlot = nullptr;
|
||||
|
||||
FBox2f GetIconUVs(int32 IconIndex);
|
||||
FMargin GetScaledMargins() const;
|
||||
int32 ChooseIcon(bool Playstation, FKey Key) const;
|
||||
void ChooseAppearance(int32 &OutIcon, FString &OutGlyph);
|
||||
bool OnTick(float DeltaTime);
|
||||
|
||||
FTSTicker::FDelegateHandle TickHandle;
|
||||
};
|
||||
398
Source/Integration/RadialMenu.cpp
Normal file
398
Source/Integration/RadialMenu.cpp
Normal 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();
|
||||
}
|
||||
164
Source/Integration/RadialMenu.h
Normal file
164
Source/Integration/RadialMenu.h
Normal 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;
|
||||
};
|
||||
226
Source/Integration/ReadSlashCommand.cpp
Normal file
226
Source/Integration/ReadSlashCommand.cpp
Normal 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user