Compare commits
57 Commits
ae1ad7640d
...
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 | |||
| ec983951fe | |||
| 9787522ef6 | |||
| 0d607ba277 | |||
| 4c1eebab96 | |||
| f3e1daf4fe | |||
| 21d8c40005 | |||
| 275698c5aa | |||
| dabb5b8f0b | |||
| fd970f20c3 | |||
| 6388de9b39 | |||
| 7a09da8a4e | |||
| d396f394ab | |||
| f19e8ccb72 | |||
| 6b057d1514 | |||
| 392faff205 |
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 |
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
[/Script/Engine.Engine]
|
||||
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient
|
||||
GameViewportClientClassName=/Script/Integration.lxViewportClient
|
||||
|
||||
[/Script/EngineSettings.GameMapsSettings]
|
||||
GameDefaultMap=/Game/LpxLevel.LpxLevel
|
||||
|
||||
@@ -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.
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,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,19 +1,12 @@
|
||||
|
||||
* UE Wingman Function Overrides.
|
||||
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
--- Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py.orig 2026-04-12 22:58:33.989318455 -0400
|
||||
+++ Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py 2025-11-10 23:34:18.481538118 -0500
|
||||
@@ -32,7 +32,7 @@
|
||||
if DataVal == 0:
|
||||
Val = 'NULL'
|
||||
else:
|
||||
- Expr = '(char16_t*)(%s)' % Data
|
||||
+ Expr = '(char16_t*)(%s)' % DataVal
|
||||
ValRef = valobj.CreateValueFromExpression('string', Expr)
|
||||
Val = ValRef.GetSummary()
|
||||
elif Type.IsReferenceType():
|
||||
@@ -47,6 +47,11 @@
|
||||
Expr = '(char16_t*)(%s)' % valobj.GetAddress()
|
||||
ValRef = valobj.CreateValueFromExpression('string', Expr)
|
||||
Val = ValRef.GetSummary()
|
||||
+ else:
|
||||
+ DataVal = valobj.GetValueAsUnsigned(0)
|
||||
+ Expr = '(char16_t)(%s)' % DataVal
|
||||
+ ValRef = valobj.CreateValueFromExpression('string', Expr)
|
||||
+ Val = ValRef.GetSummary()
|
||||
return Val
|
||||
|
||||
def UESignedCharSummaryProvider(valobj,dict):
|
||||
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-04-12 22:58:34.075320964 -0400
|
||||
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-04-12 23:03:40.139226303 -0400
|
||||
@@ -149,7 +149,7 @@
|
||||
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,
|
||||
@@ -123,10 +127,11 @@
|
||||
"type": "lldb",
|
||||
"console": "integratedTerminal",
|
||||
"initCommands": [
|
||||
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
|
||||
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
|
||||
"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()\""
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -146,8 +151,10 @@
|
||||
"type": "lldb",
|
||||
"console": "integratedTerminal",
|
||||
"initCommands": [
|
||||
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
|
||||
"settings set target.inline-breakpoint-strategy always"
|
||||
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -160,7 +167,9 @@
|
||||
"type": "lldb",
|
||||
"console": "integratedTerminal",
|
||||
"initCommands": [
|
||||
"settings set target.inline-breakpoint-strategy always"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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::NodeContextMenus();
|
||||
UWingManualSections::VariableGettersAndSetters();
|
||||
UWingManualSections::BestPerformance();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Execute all.
|
||||
for (const FSpawnNodeEntry &Entry : Entries)
|
||||
{
|
||||
UEdGraphNode* NewNode = Entry.Action->Execute(FVector2D(Entry.PosX, Entry.PosY));
|
||||
UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(PosX, PosY));
|
||||
if (NewNode)
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Entry.Type);
|
||||
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Type);
|
||||
WingGraphExport Export(NewNode, false, true);
|
||||
WingOut::Stdout.Print(Export.GetOutput());
|
||||
return;
|
||||
}
|
||||
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="Query string, can contain * wildcards"))
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (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,
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
if (!TargetGraph) return;
|
||||
|
||||
FWingGraphActions GraphActions(TargetGraph);
|
||||
for (const FString& Query : Queries.Argv)
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
|
||||
for (const FWingGraphAction* Action : Results)
|
||||
{
|
||||
@@ -56,4 +59,5 @@ public:
|
||||
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
59
Plugins/UEWingman/Source/UEWingman/Handlers/Test_TMaps.h
Normal file
59
Plugins/UEWingman/Source/UEWingman/Handlers/Test_TMaps.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "WingServer.h"
|
||||
#include "WingBasics.h"
|
||||
#include "Containers/BitArray.h"
|
||||
#include "Containers/SparseArray.h"
|
||||
#include "Test_TMaps.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UWing_Test_TMaps : public UWingHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Register() override
|
||||
{
|
||||
UWingServer::AddHandler(this,
|
||||
TEXT("Constructs a small TMap, TBitArray, and TSparseArray so that "
|
||||
"a developer can set a breakpoint and inspect them with the "
|
||||
"lldb data formatters."));
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
TMap<int32, FString> Map;
|
||||
Map.Add(1, TEXT("one"));
|
||||
Map.Add(2, TEXT("two"));
|
||||
Map.Add(3, TEXT("three"));
|
||||
Map.Add(42, TEXT("forty-two"));
|
||||
|
||||
TBitArray<> Bits;
|
||||
Bits.Add(true);
|
||||
Bits.Add(false);
|
||||
Bits.Add(true);
|
||||
Bits.Add(true);
|
||||
Bits.Add(false);
|
||||
|
||||
// Add a few entries, then remove a middle one so the live set is
|
||||
// non-contiguous. Exercises the sparse-array formatter against a hole.
|
||||
TSparseArray<FString> Sparse;
|
||||
Sparse.Add(TEXT("alpha"));
|
||||
int32 BetaIdx = Sparse.Add(TEXT("beta"));
|
||||
Sparse.Add(TEXT("gamma"));
|
||||
Sparse.Add(TEXT("delta"));
|
||||
Sparse.RemoveAt(BetaIdx);
|
||||
|
||||
TTuple<int32, FString, float, bool, FName> Tuple(1, TEXT("hello"), 3.14f, true, FName("world"));
|
||||
|
||||
// Set a breakpoint on the following line to inspect Map, Bits, and Sparse.
|
||||
WingOut::Stdout.Printf(TEXT("Test_TMaps: Map has %d entries, Bits has %d bits, Sparse has %d entries.\n"),
|
||||
Map.Num(), Bits.Num(), Sparse.Num());
|
||||
}
|
||||
};
|
||||
@@ -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="Query string, can contain *"))
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (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,
|
||||
@@ -32,6 +32,9 @@ public:
|
||||
virtual void Handle() override
|
||||
{
|
||||
WingWidgets Widgets;
|
||||
for (const FString& Query : Queries.Argv)
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
|
||||
for (const WingWidgets::Type& Entry : Results)
|
||||
{
|
||||
@@ -47,4 +50,5 @@ public:
|
||||
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
|
||||
if (!StructProp)
|
||||
{
|
||||
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->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);
|
||||
Ptr->Object = WProp->Object.Get();
|
||||
Ptr->StructType = FSProp->Struct;
|
||||
Ptr->StructBase = FSProp->ContainerPtrToValuePtr<void>(WProp->Container);
|
||||
Ptr->Editable = WProp->Editable;
|
||||
SetObj(Ptr);
|
||||
return *this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Errors.Printf(TEXT("Property %s is not a struct property.\n"),
|
||||
*WProp->Prop->GetName());
|
||||
return SetError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
WingOut::Stdout.Print(TEXT(")\n"));
|
||||
else
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
|
||||
}
|
||||
}
|
||||
WingOut::Stdout.Print(TEXT("\n"));
|
||||
}
|
||||
|
||||
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
||||
@@ -26,27 +32,17 @@ 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)
|
||||
@@ -118,13 +114,22 @@ 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 We have our own syntax for variable declarations:"
|
||||
"\n"
|
||||
"\n Array<Actor> Actors"
|
||||
"\n Float F (InstanceEditable)"
|
||||
"\n String S = This is the default value"
|
||||
"\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 "
|
||||
@@ -199,22 +204,33 @@ void UWingManualSections::VariableGettersAndSetters()
|
||||
));
|
||||
}
|
||||
|
||||
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 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"
|
||||
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
|
||||
"\n to get detailed help for a specific command."
|
||||
"\n"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -255,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)
|
||||
@@ -277,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);
|
||||
@@ -341,7 +290,11 @@ FString FWingProperty::GetText() const
|
||||
return TEXT("None");
|
||||
}
|
||||
FString Result;
|
||||
Prop->ExportTextItem_InContainer(Result, Container, nullptr, nullptr, PPF_None);
|
||||
// DefaultValue == PropertyValue makes ExportText_Direct's Data==Delta
|
||||
// short-circuit fire for every subfield, so default-valued fields still
|
||||
// get emitted (e.g. SizeRule=Automatic on FSlateChildSize).
|
||||
const void* Value = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||
Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, nullptr, PPF_None);
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -374,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;
|
||||
}
|
||||
@@ -432,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
|
||||
@@ -461,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.
|
||||
@@ -470,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,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;
|
||||
int32 ArgIndex = 0;
|
||||
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
|
||||
{
|
||||
FWingProperty& P = Props[PropIndex];
|
||||
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
|
||||
const bool bIsRest =
|
||||
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||
|
||||
// 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)
|
||||
if (bIsRest)
|
||||
{
|
||||
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
|
||||
if (!KnownKeys.Contains(Name))
|
||||
if (PropIndex + 1 != Props.Num())
|
||||
{
|
||||
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
||||
Ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each property from JSON
|
||||
for (FWingProperty& P : Props)
|
||||
{
|
||||
FString JsonKey = WingUtils::FormatName(P.Prop);
|
||||
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
|
||||
if (!Value)
|
||||
{
|
||||
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
|
||||
if (!Optional)
|
||||
{
|
||||
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
|
||||
Ok = false;
|
||||
}
|
||||
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)
|
||||
{
|
||||
Errors.Printf(TEXT("property data should be stored in a json object\n"));
|
||||
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
|
||||
*WingUtils::FormatName(P.Prop));
|
||||
return false;
|
||||
}
|
||||
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
|
||||
|
||||
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 (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("ERROR: Too many parameters, starting with '%s'\n"),
|
||||
*Argv[ArgIndex]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -576,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,7 +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)
|
||||
{
|
||||
TArray<FString> Argv;
|
||||
FString ResponseText;
|
||||
|
||||
if (DeserializeArgv(RequestBytes, Argv))
|
||||
{
|
||||
PreCallHandler();
|
||||
TryCallHandler(Argv);
|
||||
ResponseText = PostCallHandler();
|
||||
}
|
||||
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
|
||||
|
||||
FTCHARToUTF8 Utf8(*ResponseText);
|
||||
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
||||
}
|
||||
|
||||
void UWingServer::PreCallHandler()
|
||||
{
|
||||
LogCapture.CapturedErrors.Empty();
|
||||
LogCapture.bEnabled = true;
|
||||
@@ -177,9 +194,10 @@ FString UWingServer::HandleRequest(const FString& Line)
|
||||
SuggestedManualSections.Empty();
|
||||
bSuggestHandlerHelp = false;
|
||||
LastHandler = nullptr;
|
||||
}
|
||||
|
||||
TryCallHandler(Line);
|
||||
|
||||
FString UWingServer::PostCallHandler()
|
||||
{
|
||||
Notifier.SendNotifications();
|
||||
LogCapture.bEnabled = false;
|
||||
for (const FString& Msg : LogCapture.CapturedErrors)
|
||||
@@ -202,34 +220,24 @@ FString UWingServer::HandleRequest(const FString& Line)
|
||||
}
|
||||
FString Result = WingOut::StdoutBuffer.ToString();
|
||||
WingOut::StdoutBuffer.Reset();
|
||||
for (int32 i = 0; i < Result.Len(); ++i)
|
||||
{
|
||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UWingServer::TryCallHandler(const FString &Line)
|
||||
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
|
||||
{
|
||||
// Turn the request string into a JSON tree.
|
||||
TSharedPtr<FJsonObject> Request;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
||||
FJsonSerializer::Deserialize(Reader, Request);
|
||||
if (!Request.IsValid())
|
||||
FString Command = "Documentation_Manual";
|
||||
if (Argv.Num() > 0)
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
|
||||
return;
|
||||
Command = Argv[0];
|
||||
Argv = Argv.RightChop(1);
|
||||
}
|
||||
|
||||
// Extract the command from the request.
|
||||
FString Command;
|
||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
||||
if ((Command.Equals(TEXT("--help"))) ||
|
||||
(Command.Equals(TEXT("-help"))) ||
|
||||
(Command.Equals(TEXT("help"))))
|
||||
{
|
||||
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
|
||||
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
|
||||
return;
|
||||
Command = "Documentation_Manual";
|
||||
}
|
||||
Request->RemoveField(TEXT("command"));
|
||||
|
||||
// Find the handler for the specified command.
|
||||
FWingHandlerConfig* Found = FindHandler(Command);
|
||||
@@ -246,9 +254,9 @@ void UWingServer::TryCallHandler(const FString &Line)
|
||||
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;
|
||||
@@ -306,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;
|
||||
Client->bDone = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<uint8> 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;
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
if (RequestBytes.Num() - Offset < 4)
|
||||
{
|
||||
Argv.Empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
|
||||
Offset += 4;
|
||||
|
||||
if ((uint32)(RequestBytes.Num() - Offset) < Length)
|
||||
{
|
||||
FMemory::Memmove(
|
||||
RecvBuf.GetData(),
|
||||
RecvBuf.GetData() + MessageLen + 1,
|
||||
RemainingBytes);
|
||||
Argv.Empty();
|
||||
return false;
|
||||
}
|
||||
RecvLen = RemainingBytes;
|
||||
|
||||
Argv.Add(FString::ConstructFromPtrSize(
|
||||
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
|
||||
Length));
|
||||
Offset += (int32)Length;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (RecvBuf.Num() >= MaxRecvBufBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
|
||||
UnusedSpace = RecvBuf.Num() - RecvLen;
|
||||
}
|
||||
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)
|
||||
{
|
||||
uint8 Temp[ChunkSize];
|
||||
int32 BytesRead = 0;
|
||||
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
|
||||
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (BytesRead <= 0) break;
|
||||
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (BytesRead <= 0)
|
||||
{
|
||||
return false;
|
||||
RecvBuf.Append(Temp, BytesRead);
|
||||
}
|
||||
|
||||
RecvLen += BytesRead;
|
||||
if (RecvBuf.Num() < 4) return false;
|
||||
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
|
||||
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
|
||||
RecvBuf.RemoveAt(0, 4);
|
||||
|
||||
OutRequest = MoveTemp(RecvBuf);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -421,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);
|
||||
@@ -484,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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "WingServer.generated.h"
|
||||
|
||||
class FSocket;
|
||||
class FJsonObject;
|
||||
|
||||
/**
|
||||
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
||||
@@ -72,13 +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);
|
||||
void TryCallHandler(const FString &Line);
|
||||
// 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;
|
||||
@@ -95,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,162 +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 = 120
|
||||
|
||||
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()
|
||||
# Retry once in case the connection was stale
|
||||
if connect():
|
||||
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 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:
|
||||
text = result["error"]
|
||||
else:
|
||||
text = result
|
||||
return make_jsonrpc(msg_id, {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
})
|
||||
|
||||
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,17 +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)
|
||||
print(json.dumps(parsed, indent=2))
|
||||
except json.JSONDecodeError:
|
||||
print(result)
|
||||
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,6 +105,13 @@ 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
|
||||
|
||||
@@ -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,112 +0,0 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InputEvents.cpp
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#include "InputEvents.h"
|
||||
#include "Common.h"
|
||||
|
||||
bool FlxEventRequest::operator==(const FlxEventRequest &Other) const
|
||||
{
|
||||
return (Widget == Other.Widget) &&
|
||||
(UseUIOnly == Other.UseUIOnly) &&
|
||||
(ShowPointer == Other.ShowPointer) &&
|
||||
(Hotkeys == Other.Hotkeys);
|
||||
}
|
||||
|
||||
bool FlxEventRequests::SanityCheck(const FlxEventRequest &Request)
|
||||
{
|
||||
if (Request.Widget == nullptr)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
|
||||
return false;
|
||||
}
|
||||
if (Request.ShowPointer && !Request.UseUIOnly)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: ShowPointer requires UseUIOnly."));
|
||||
return false;
|
||||
}
|
||||
if (Request.UseUIOnly && !Request.Hotkeys.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: Widget asked for all events, and also, specific events"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlxEventRequests::SplitHighLow(View &High, View &Low)
|
||||
{
|
||||
int32 NumHigh = 0;
|
||||
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++;
|
||||
int32 NumLow = Requests.Num() - NumHigh;
|
||||
High = View(Requests.GetData(), NumHigh);
|
||||
Low = View(Requests.GetData() + NumHigh, NumLow);
|
||||
}
|
||||
|
||||
void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
|
||||
{
|
||||
// Divide the array into a high-priority slice and a low-priority slice.
|
||||
View High, Low;
|
||||
SplitHighLow(High, Low);
|
||||
|
||||
// This is a simple test to see if anything is going to change.
|
||||
// If not, we return early and avoid setting the dirty bit.
|
||||
if (NewRequest.UseUIOnly)
|
||||
{
|
||||
if ((High.Num() > 0) && (High[0] == NewRequest)) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((Low.Num() > 0) && (Low[0] == NewRequest)) return;
|
||||
}
|
||||
|
||||
// We're going to build a new version of the requests array.
|
||||
TArray<FlxEventRequest> Updated;
|
||||
|
||||
// Add all high priority requests to the updated array, new request first.
|
||||
if (NewRequest.UseUIOnly) Updated.Add(NewRequest);
|
||||
for (const FlxEventRequest &Req : High)
|
||||
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
|
||||
|
||||
// Add all low priority requests to the updated array, new request first.
|
||||
if (!NewRequest.UseUIOnly) Updated.Add(NewRequest);
|
||||
for (const FlxEventRequest &Req : Low)
|
||||
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
|
||||
|
||||
Swap(Requests, Updated);
|
||||
Dirty = true;
|
||||
}
|
||||
|
||||
void FlxEventRequests::Remove(UUserWidget *Widget)
|
||||
{
|
||||
int32 N = Requests.Num();
|
||||
Requests.RemoveAll([Widget](const FlxEventRequest &Entry)
|
||||
{
|
||||
return Entry.Widget == Widget;
|
||||
});
|
||||
if (Requests.Num() < N) Dirty = true;
|
||||
}
|
||||
|
||||
void FlxEventRequests::GarbageCollect()
|
||||
{
|
||||
int32 N = Requests.Num();
|
||||
Requests.RemoveAll([](const FlxEventRequest &Entry)
|
||||
{
|
||||
UUserWidget *W = Entry.Widget;
|
||||
return W == nullptr || !IsValid(W) || W->GetParent() == nullptr;
|
||||
});
|
||||
if (Requests.Num() < N) Dirty = true;
|
||||
}
|
||||
|
||||
FlxEventRequests::InputMode FlxEventRequests::GetRequestedMode() const
|
||||
{
|
||||
if ((Requests.Num() > 0) && (Requests[0].UseUIOnly))
|
||||
{
|
||||
return InputMode::UIOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
return InputMode::GameOnly;
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InputEvents.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 "InputCoreTypes.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InputEvents.generated.h"
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FlxEventRequest
|
||||
//
|
||||
// A widget's declaration of interest in input events.
|
||||
//
|
||||
// Widget: The widget that wants to receive events.
|
||||
// UseUIOnly: If true, activating this request puts
|
||||
// the system into UIOnly mode.
|
||||
// ShowPointer: If true, the mouse pointer should be
|
||||
// visible when this widget has control.
|
||||
// Hotkeys: Keys that go to this widget when the
|
||||
// player is in GameOnly mode.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FlxEventRequest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FlxEventRequest() = default;
|
||||
FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray<FKey> &InHotkeys)
|
||||
: Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {}
|
||||
|
||||
bool operator == (const FlxEventRequest &Other) const;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
UUserWidget* Widget = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
bool UseUIOnly = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
bool ShowPointer = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite)
|
||||
TArray<FKey> Hotkeys;
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
struct FlxEventRequests
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
// High priority requests are always before low-priority.
|
||||
// Otherwise, these are in order of most recent first.
|
||||
TArray<FlxEventRequest> Requests;
|
||||
|
||||
UPROPERTY()
|
||||
bool Dirty = true;
|
||||
|
||||
public:
|
||||
enum class InputMode { UIOnly, GameOnly };
|
||||
|
||||
using View = TArrayView<FlxEventRequest>;
|
||||
|
||||
// Get the requests array.
|
||||
const TArray<FlxEventRequest> &GetRequests() const { return Requests; }
|
||||
|
||||
// Sanity check a request to see if it is reasonable.
|
||||
static bool SanityCheck(const FlxEventRequest &Request);
|
||||
|
||||
// Divide Requests into a high-priority slice and a low-priority slice.
|
||||
void SplitHighLow(View &High, View &Low);
|
||||
|
||||
// Apply a request. Replaces any previous request by the same widget.
|
||||
void Request(const FlxEventRequest &NewRequest);
|
||||
|
||||
// Remove all requests by the specified widget.
|
||||
void Remove(UUserWidget *Widget);
|
||||
|
||||
// Remove any requests by dead widgets or widgets with no parents.
|
||||
void GarbageCollect();
|
||||
|
||||
// Return true if the requests have changed since the last ClearDirty.
|
||||
bool IsDirty() { return Dirty; }
|
||||
|
||||
// Clear the dirty flag.
|
||||
void ClearDirty() { Dirty = false; }
|
||||
|
||||
// Set the dirty flag.
|
||||
void SetDirty() { Dirty = true; }
|
||||
|
||||
// Get the currently-requested mode.
|
||||
InputMode GetRequestedMode() const;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -172,7 +172,7 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
|
||||
if (PC != nullptr)
|
||||
{
|
||||
PC->UpdateLookAt();
|
||||
PC->UpdateEventDispatch();
|
||||
PC->UpdateInputMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ void UlxUserWidget::BackupInputComponent()
|
||||
}
|
||||
}
|
||||
|
||||
void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction)
|
||||
void UlxUserWidget::DisableInputAction(const UInputAction* InputAction)
|
||||
{
|
||||
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
|
||||
if (!EIC) return;
|
||||
@@ -40,9 +40,9 @@ void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction)
|
||||
});
|
||||
}
|
||||
|
||||
void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction)
|
||||
void UlxUserWidget::RestoreInputAction(const UInputAction* InputAction)
|
||||
{
|
||||
DisableEventBinding(InputAction);
|
||||
DisableInputAction(InputAction);
|
||||
|
||||
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
|
||||
if (!EIC) return;
|
||||
@@ -59,7 +59,7 @@ void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction)
|
||||
|
||||
void UlxUserWidget::RedirectInputAction(const UInputAction* From, const UInputAction* To)
|
||||
{
|
||||
DisableEventBinding(From);
|
||||
DisableInputAction(From);
|
||||
|
||||
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
|
||||
if (!EIC) return;
|
||||
|
||||
@@ -22,20 +22,18 @@ public:
|
||||
// from the component and reinstated without losing their delegates.
|
||||
void BackupInputComponent();
|
||||
|
||||
// Remove every live event binding whose action is InputAction.
|
||||
// No-op if there are none, or if InputComponent isn't enhanced.
|
||||
// Removes all handlers for 'InputAction'. That includes temporarily
|
||||
// deactivating event graph nodes that handle 'InputAction'.
|
||||
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
|
||||
void DisableEventBinding(const UInputAction* InputAction);
|
||||
void DisableInputAction(const UInputAction* InputAction);
|
||||
|
||||
// Replace any live bindings for InputAction with fresh clones of every
|
||||
// saved binding for that action. Leaves the backup array intact so this
|
||||
// can be called repeatedly.
|
||||
// Reactivates any graph nodes that handle 'InputAction', and
|
||||
// removes any other handlers for 'InputAction'.
|
||||
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
|
||||
void RestoreInputBinding(const UInputAction* InputAction);
|
||||
void RestoreInputAction(const UInputAction* InputAction);
|
||||
|
||||
// Install live bindings on From that, when fired, dispatch through a
|
||||
// clone of each saved binding for To. Clears any pre-existing live
|
||||
// bindings on From first. Backup array is untouched.
|
||||
// Any event graph nodes that handle 'to' are made to also
|
||||
// handle 'From' events. Any other handlers of 'From' are removed.
|
||||
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
|
||||
void RedirectInputAction(const UInputAction* From, const UInputAction* To);
|
||||
|
||||
|
||||
63
Source/Integration/LuprexViewportClient.cpp
Normal file
63
Source/Integration/LuprexViewportClient.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// LuprexViewportClient.cpp
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#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 "Slate/SObjectWidget.h"
|
||||
|
||||
UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer)
|
||||
: Super(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. 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() && TryBringToFront(Path)) return true;
|
||||
}
|
||||
|
||||
return Super::InputKey(EventArgs);
|
||||
}
|
||||
32
Source/Integration/LuprexViewportClient.h
Normal file
32
Source/Integration/LuprexViewportClient.h
Normal file
@@ -0,0 +1,32 @@
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// LuprexViewportClient.h
|
||||
//
|
||||
// Custom game viewport client. Implements a project-wide
|
||||
// click-to-focus rule: when a left-mouse-button click is
|
||||
// not handled by any widget and bubbles up to the viewport,
|
||||
// we hit-test under the cursor and focus the nearest
|
||||
// focusable ancestor of whatever was hit.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
#include "Layout/WidgetPath.h"
|
||||
#include "LuprexViewportClient.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class INTEGRATION_API UlxViewportClient : public UGameViewportClient
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UlxViewportClient(const FObjectInitializer &ObjectInitializer);
|
||||
|
||||
virtual bool InputKey(const FInputKeyEventArgs &EventArgs) override;
|
||||
|
||||
private:
|
||||
bool TryBringToFront(const FWidgetPath &Path);
|
||||
};
|
||||
@@ -1,27 +1,21 @@
|
||||
#include "PlayerControllerBase.h"
|
||||
#include "Common.h"
|
||||
#include "GameFramework/InputDeviceSubsystem.h"
|
||||
#include "GameFramework/InputSettings.h"
|
||||
#include "RootCanvas.h"
|
||||
#include "Tangible.h"
|
||||
#include "TangibleManager.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "Components/InputComponent.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
#include "Engine/LevelScriptActor.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Framework/Application/SlateUser.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Widgets/SViewport.h"
|
||||
#include "Slate/SObjectWidget.h"
|
||||
|
||||
FString AlxPlayerControllerBase::GetUserWidgetName(SWidget *W)
|
||||
{
|
||||
while (W)
|
||||
{
|
||||
if (W->GetType() == FName("SObjectWidget"))
|
||||
{
|
||||
UUserWidget *UW = static_cast<SObjectWidget*>(W)->GetWidgetObject();
|
||||
if (UW) return UW->GetClass()->GetName();
|
||||
}
|
||||
W = W->GetParentWidget().Get();
|
||||
}
|
||||
return TEXT("Unknown Widget");
|
||||
}
|
||||
|
||||
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
|
||||
{
|
||||
@@ -73,92 +67,257 @@ 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();
|
||||
HotkeyInputComponent = NewObject<UInputComponent>(this);
|
||||
HotkeyInputComponent->bBlockInput = false;
|
||||
PushInputComponent(HotkeyInputComponent);
|
||||
|
||||
if (FSlateApplication::IsInitialized())
|
||||
{
|
||||
FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject(
|
||||
this, &AlxPlayerControllerBase::HandleFocusChanging);
|
||||
}
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::UpdateEventDispatch()
|
||||
void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
EventRequests.GarbageCollect();
|
||||
|
||||
// If we're in GameOnly mode, check that focus is still on the viewport.
|
||||
if (CurrentInputMode == InputMode::GameOnly)
|
||||
if (FocusChangingHandle.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
UGameViewportClient *GVC = GetWorld() ? GetWorld()->GetGameViewport() : nullptr;
|
||||
if (GVC)
|
||||
{
|
||||
TSharedPtr<SViewport> ViewportWidget = GVC->GetGameViewportWidget();
|
||||
if (ViewportWidget.IsValid())
|
||||
{
|
||||
TSharedPtr<SWidget> Focused = FSlateApplication::Get().GetKeyboardFocusedWidget();
|
||||
if (Focused.Get() != ViewportWidget.Get())
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, keyboard focus must stay on viewport, but was stolen by: %s. Restoring."), *GetUserWidgetName(Focused.Get()));
|
||||
EventRequests.SetDirty();
|
||||
FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle);
|
||||
FocusChangingHandle.Reset();
|
||||
}
|
||||
if (!ViewportWidget->HasMouseCapture())
|
||||
|
||||
if (IsValid(RootWidget))
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, viewport must have mouse capture, but lost it. Restoring."));
|
||||
EventRequests.SetDirty();
|
||||
RootWidget->RemoveFromParent();
|
||||
}
|
||||
RootWidget = nullptr;
|
||||
RootCanvas = nullptr;
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::HandleFocusChanging(
|
||||
const FFocusEvent &FocusEvent,
|
||||
const FWeakWidgetPath &OldPath,
|
||||
const TSharedPtr<SWidget> &OldFocusedWidget,
|
||||
const FWidgetPath &NewPath,
|
||||
const TSharedPtr<SWidget> &NewFocusedWidget)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Display,
|
||||
TEXT("Focus changing: '%s' -> '%s' (cause: %s)"),
|
||||
OldFocusedWidget.IsValid() ? *OldFocusedWidget->GetTypeAsString() : TEXT("<none>"),
|
||||
NewFocusedWidget.IsValid() ? *NewFocusedWidget->GetTypeAsString() : TEXT("<none>"),
|
||||
*UEnum::GetValueAsString(FocusEvent.GetCause()));
|
||||
}
|
||||
|
||||
UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *Widget)
|
||||
{
|
||||
if (!IsValid(Widget)) return nullptr;
|
||||
|
||||
// Cache the FProperty on first call. FProperties are owned by
|
||||
// the native UUserWidget UClass, which lives for the process
|
||||
// lifetime, so the pointer stays valid without us needing to
|
||||
// root anything against GC. Static local init is thread-safe.
|
||||
static FObjectProperty *InputComponentProp = FindFProperty<FObjectProperty>(
|
||||
UUserWidget::StaticClass(), TEXT("InputComponent"));
|
||||
check(InputComponentProp);
|
||||
|
||||
UObject *Value = InputComponentProp->GetObjectPropertyValue_InContainer(Widget);
|
||||
return Cast<UInputComponent>(Value);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
|
||||
{
|
||||
if (!IsValid(Widget))
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("AddWidgetToRoot called with an invalid widget."));
|
||||
return;
|
||||
}
|
||||
APlayerController *OwningPC = Widget->GetOwningPlayer();
|
||||
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(OwningPC);
|
||||
if (PC == nullptr)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error,
|
||||
TEXT("AddWidgetToRoot: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
|
||||
*Widget->GetName(), *GetNameSafe(OwningPC));
|
||||
return;
|
||||
}
|
||||
if (PC->RootCanvas == nullptr)
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error,
|
||||
TEXT("AddWidgetToRoot: root canvas is not initialized, this is probably an initialization order issue"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Widget->GetParent() == PC->RootCanvas) return;
|
||||
|
||||
PC->RootCanvas->AddChildToRootCanvas(Widget);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
|
||||
{
|
||||
// Controlled pawn gets last dibs on the input stack
|
||||
APawn* ControlledPawn = GetPawnOrSpectator();
|
||||
if (ControlledPawn)
|
||||
{
|
||||
if (ControlledPawn->InputEnabled())
|
||||
{
|
||||
// Get the explicit input component that is created upon Pawn possession. This one gets last dibs.
|
||||
if (ControlledPawn->InputComponent)
|
||||
{
|
||||
InputStack.Push(ControlledPawn->InputComponent);
|
||||
}
|
||||
|
||||
// See if there is another InputComponent that was added to the Pawn's components array (possibly by script).
|
||||
for (UActorComponent* ActorComponent : ControlledPawn->GetComponents())
|
||||
{
|
||||
UInputComponent* PawnInputComponent = Cast<UInputComponent>(ActorComponent);
|
||||
if (PawnInputComponent && PawnInputComponent != ControlledPawn->InputComponent)
|
||||
{
|
||||
InputStack.Push(PawnInputComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!EventRequests.IsDirty()) return;
|
||||
EventRequests.ClearDirty();
|
||||
|
||||
CurrentInputMode = EventRequests.GetRequestedMode();
|
||||
const TArray<FlxEventRequest> &Requests = EventRequests.GetRequests();
|
||||
|
||||
if (CurrentInputMode == InputMode::UIOnly)
|
||||
// LevelScriptActors are put on the stack next
|
||||
for (ULevel* Level : GetWorld()->GetLevels())
|
||||
{
|
||||
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget()));
|
||||
ALevelScriptActor* ScriptActor = Level->GetLevelScriptActor();
|
||||
if (ScriptActor)
|
||||
{
|
||||
if (ScriptActor->InputEnabled() && ScriptActor->InputComponent)
|
||||
{
|
||||
InputStack.Push(ScriptActor->InputComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (InputEnabled())
|
||||
{
|
||||
InputStack.Push(InputComponent);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
|
||||
UInputComponent *IC = GetWidgetInputComponent(Widget);
|
||||
if (IC) WidgetManagedComponents.Add(IC);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
UInputComponent* IC = CurrentInputStack[Idx].Get();
|
||||
if (IsValid(IC))
|
||||
{
|
||||
if (!WidgetManagedComponents.Contains(IC)) InputStack.Push(IC);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetInputMode(FInputModeGameOnly());
|
||||
|
||||
HotkeyInputComponent->KeyBindings.Empty();
|
||||
TSet<FKey> BoundKeys;
|
||||
for (const FlxEventRequest &Req : Requests)
|
||||
{
|
||||
for (const FKey &Key : Req.Hotkeys)
|
||||
{
|
||||
if (!BoundKeys.Contains(Key))
|
||||
{
|
||||
BoundKeys.Add(Key);
|
||||
HotkeyInputComponent->BindKey(Key, IE_Pressed, this, &AlxPlayerControllerBase::ForwardKeyEvent);
|
||||
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::ForwardKeyEvent(FKey Key)
|
||||
void AlxPlayerControllerBase::UpdateInputMode()
|
||||
{
|
||||
// TODO: implement
|
||||
}
|
||||
// Get all the various objects we need to be able to manipulate
|
||||
// the input mode.
|
||||
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
|
||||
ULocalPlayer *LocalPlayer = Cast<ULocalPlayer>(Player);
|
||||
if (GameViewportClient == nullptr || LocalPlayer == nullptr) return;
|
||||
TSharedPtr<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget();
|
||||
if (!ViewportWidget.IsValid()) return;
|
||||
TSharedRef<SViewport> ViewportWidgetRef = ViewportWidget.ToSharedRef();
|
||||
FReply &SlateOperations = LocalPlayer->GetSlateOperations();
|
||||
TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser();
|
||||
if (!SlateUser.IsValid()) return;
|
||||
|
||||
|
||||
void AlxPlayerControllerBase::RequestEvents(const FlxEventRequest &Request)
|
||||
{
|
||||
if (!FlxEventRequests::SanityCheck(Request)) return;
|
||||
AlxPlayerControllerBase *PC = FromContext(Request.Widget);
|
||||
PC->EventRequests.Request(Request);
|
||||
}
|
||||
RootCanvas->UpdateZOrders();
|
||||
|
||||
void AlxPlayerControllerBase::UnRequestEvents(UUserWidget *Widget)
|
||||
{
|
||||
if (Widget == nullptr)
|
||||
// Get the desired configuration from the top widget.
|
||||
UUserWidget *Widget = nullptr;
|
||||
UWidget *Focus = nullptr;
|
||||
bool ShowPointer = false;
|
||||
if (UlxRootCanvasSlot *Top = RootCanvas->GetTopWidget())
|
||||
{
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("UnRequestEvents called with null widget."));
|
||||
return;
|
||||
Widget = Cast<UUserWidget>(Top->GetContent());
|
||||
Focus = Widget->GetDesiredFocusWidget();
|
||||
ShowPointer = Top->ShowPointer;
|
||||
}
|
||||
|
||||
SetShowMouseCursor(ShowPointer);
|
||||
|
||||
if (ShowPointer)
|
||||
{
|
||||
// Only release capture if the viewport is currently holding it
|
||||
// (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture
|
||||
// every tick would yank capture away from widgets mid-gesture
|
||||
// (scrollbar drags, slider drags, etc.).
|
||||
if (SlateUser->DoesWidgetHaveAnyCapture(ViewportWidget))
|
||||
{
|
||||
SlateOperations.ReleaseMouseCapture();
|
||||
}
|
||||
SlateOperations.ReleaseMouseLock();
|
||||
GameViewportClient->SetMouseLockMode(EMouseLockMode::DoNotLock);
|
||||
GameViewportClient->SetHideCursorDuringCapture(false);
|
||||
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CaptureDuringMouseDown);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also captures the mouse to the viewport.
|
||||
SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
|
||||
SlateOperations.LockMouseToWidget(ViewportWidgetRef);
|
||||
GameViewportClient->SetMouseLockMode(EMouseLockMode::LockOnCapture);
|
||||
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CapturePermanently);
|
||||
}
|
||||
|
||||
GameViewportClient->SetIgnoreInput(false);
|
||||
|
||||
// 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 (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
|
||||
{
|
||||
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SlateOperations.SetUserFocus(ViewportWidgetRef);
|
||||
}
|
||||
AlxPlayerControllerBase *PC = FromContext(Widget);
|
||||
PC->EventRequests.Remove(Widget);
|
||||
}
|
||||
|
||||
void AlxPlayerControllerBase::UpdateLookAt()
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Common.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputEvents.h"
|
||||
#include "UObject/ObjectKey.h"
|
||||
#include "PlayerControllerBase.generated.h"
|
||||
|
||||
class UlxRootCanvasPanel;
|
||||
class UWidget;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
using InputMode = FlxEventRequests::InputMode;
|
||||
|
||||
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
||||
static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
|
||||
|
||||
@@ -29,12 +31,6 @@ public:
|
||||
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
|
||||
static void SetLookAtChanged(const UObject *Context);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
|
||||
static void RequestEvents(const FlxEventRequest &Request);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
|
||||
static void UnRequestEvents(UUserWidget *Widget);
|
||||
|
||||
// Blueprint events
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
||||
void CalculateLookAt();
|
||||
@@ -42,19 +38,38 @@ public:
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
|
||||
void LookAtChanged();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// Called by GameMode each tick.
|
||||
void UpdateLookAt();
|
||||
|
||||
// Rebuild input component and switch input mode.
|
||||
void UpdateEventDispatch();
|
||||
// Called by GameMode each tick. GCs dead requests and will
|
||||
// eventually reconcile focus, pointer, and capture state.
|
||||
void UpdateInputMode();
|
||||
|
||||
// Handler for GameOnly mode hotkey presses.
|
||||
void ForwardKeyEvent(FKey Key);
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
// Walk up from a Slate widget to find the nearest UMG widget class name.
|
||||
static FString GetUserWidgetName(SWidget *W);
|
||||
private:
|
||||
FDelegateHandle FocusChangingHandle;
|
||||
void HandleFocusChanging(
|
||||
const struct FFocusEvent &FocusEvent,
|
||||
const class FWeakWidgetPath &OldPath,
|
||||
const TSharedPtr<class SWidget> &OldFocusedWidget,
|
||||
const class FWidgetPath &NewPath,
|
||||
const TSharedPtr<class SWidget> &NewFocusedWidget);
|
||||
|
||||
public:
|
||||
|
||||
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
|
||||
|
||||
// Read UUserWidget::InputComponent via reflection. The field is
|
||||
// protected and has no public accessor; this reaches through the
|
||||
// FProperty so we always see the current value without caching it.
|
||||
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
|
||||
|
||||
// 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);
|
||||
@@ -62,15 +77,16 @@ public:
|
||||
UPROPERTY()
|
||||
FHitResult CurrentLookAt;
|
||||
|
||||
// The single top-level UUserWidget added to the viewport. All
|
||||
// top-level UI widgets are children of RootCanvas inside it.
|
||||
UPROPERTY()
|
||||
FlxEventRequests EventRequests;
|
||||
UUserWidget *RootWidget = nullptr;
|
||||
|
||||
// Input component for GameOnly mode: catches hotkeys only.
|
||||
// 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()
|
||||
UInputComponent *HotkeyInputComponent = nullptr;
|
||||
|
||||
// Current input mode.
|
||||
InputMode CurrentInputMode = InputMode::GameOnly;
|
||||
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;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user