Compare commits
60 Commits
2f83910897
...
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 | |||
| ae1ad7640d | |||
| 26399a6a15 | |||
| 8a3d200247 |
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_AnyHotkey.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_AnyHotkey.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_Console.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Console.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadD.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadD.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadR.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadU.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadU.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceM.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceM.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceR.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
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/InputActions/IA_ShoulderL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_ShoulderL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_ShoulderR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_ShoulderR.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_TriggerL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_TriggerL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_TriggerR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_TriggerR.uasset
LFS
Normal file
Binary file not shown.
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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user