Compare commits
34 Commits
4680a0f3f4
...
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ UnrealEngine
|
|||||||
*.vcproj
|
*.vcproj
|
||||||
.ignore
|
.ignore
|
||||||
|
|
||||||
|
.gitdeps-cache/**
|
||||||
.vscode/**
|
.vscode/**
|
||||||
Config/**
|
Config/**
|
||||||
Saved/**
|
Saved/**
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"ue-wingman": {
|
|
||||||
"command": "python3",
|
|
||||||
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,9 +25,16 @@
|
|||||||
- `Docs/` — Documentation.
|
- `Docs/` — Documentation.
|
||||||
- `Config/` — Unreal config files
|
- `Config/` — Unreal config files
|
||||||
- `EnginePatches/` — Custom engine modifications
|
- `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
|
- `../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
|
## Coding Conventions
|
||||||
|
|
||||||
- Prefer early returns and `continue` to reduce nesting (never-nester style).
|
- Prefer early returns and `continue` to reduce nesting (never-nester style).
|
||||||
|
|||||||
1498
Art/gamepad.svg
Normal file
1498
Art/gamepad.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 65 KiB |
321
Art/radial.svg
Normal file
321
Art/radial.svg
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="200mm"
|
||||||
|
height="200mm"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="radial.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2.8284271"
|
||||||
|
inkscape:cx="365.75098"
|
||||||
|
inkscape:cy="372.82205"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1026"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid132"
|
||||||
|
spacingx="0.99999999"
|
||||||
|
spacingy="0.99999999" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="120"
|
||||||
|
y="90"
|
||||||
|
id="text2926"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2924"
|
||||||
|
style="stroke-width:1;font-size:7.05555556px"
|
||||||
|
x="120"
|
||||||
|
y="90" /></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="116"
|
||||||
|
y="87"
|
||||||
|
id="text3271"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3269"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="116"
|
||||||
|
y="87">Light the furnace</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="120.365"
|
||||||
|
y="96.362228"
|
||||||
|
id="text3325"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3323"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="120.365"
|
||||||
|
y="96.362228">Replenish Fuel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="121"
|
||||||
|
y="107"
|
||||||
|
id="text3329"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3327"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="121"
|
||||||
|
y="107">Bake chicken</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="116"
|
||||||
|
y="117"
|
||||||
|
id="text3333"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3331"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="116"
|
||||||
|
y="117">Cannibalism</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="84"
|
||||||
|
y="87"
|
||||||
|
id="text3337"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3335"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="84"
|
||||||
|
y="87">Banana Bread</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="79"
|
||||||
|
y="97"
|
||||||
|
id="text3341"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3339"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="79"
|
||||||
|
y="97">Electric Eel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="79"
|
||||||
|
y="107"
|
||||||
|
id="text3345"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3343"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="79"
|
||||||
|
y="107">Engine Fire</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="84"
|
||||||
|
y="117"
|
||||||
|
id="text3349"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3347"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="84"
|
||||||
|
y="117">Radial Menu</tspan></text>
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 89.999999,84.999999 4,-10e-7"
|
||||||
|
id="path4558"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 106,84.999998 4,10e-7"
|
||||||
|
id="path4562"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 84.999999,94.999999 3,0"
|
||||||
|
id="path4564"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 112,94.999997 3,2e-6"
|
||||||
|
id="path4566"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 84.999999,105 3,0"
|
||||||
|
id="path4568"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 112,105 3,0"
|
||||||
|
id="path4570"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 89.999999,115 4,0"
|
||||||
|
id="path4572"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#707070;stroke-width:0.3;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 106,115 4,0"
|
||||||
|
id="path4574"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
id="path4576"
|
||||||
|
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 102,100.15803 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.104575 2,-4.000005 2,-4.000005 0,0 2.000004,2.89543 2.000004,4.000005 z"
|
||||||
|
sodipodi:nodetypes="ssscs" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 112,105 -5.72673,-2.38614"
|
||||||
|
id="path1147" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="M 106.27327,97.386131 112,94.999997"
|
||||||
|
id="path1149" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="M 102.52401,93.689963 106,84.999996"
|
||||||
|
id="path1151" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="M 97.475986,93.689963 93.999999,84.999996"
|
||||||
|
id="path1153" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 106,115 -3.47599,-8.68997"
|
||||||
|
id="path1155" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 93.999999,115 3.475987,-8.68997"
|
||||||
|
id="path1157" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 87.999998,105 5.726726,-2.38614"
|
||||||
|
id="path1159" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 87.999998,94.999997 5.726726,2.386136"
|
||||||
|
id="path1161" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="path1220"
|
||||||
|
cx="102.52401"
|
||||||
|
cy="106.31003"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1274"
|
||||||
|
cx="97.475983"
|
||||||
|
cy="106.31003"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1276"
|
||||||
|
cx="93.726723"
|
||||||
|
cy="102.61386"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1278"
|
||||||
|
cx="106.27327"
|
||||||
|
cy="102.61386"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1280"
|
||||||
|
cx="106.27327"
|
||||||
|
cy="97.386131"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1282"
|
||||||
|
cx="102.52401"
|
||||||
|
cy="93.689964"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1284"
|
||||||
|
cx="90"
|
||||||
|
cy="85"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1286"
|
||||||
|
cx="93.726723"
|
||||||
|
cy="97.386131"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1290"
|
||||||
|
cx="97.475983"
|
||||||
|
cy="93.689964"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1292"
|
||||||
|
cx="110"
|
||||||
|
cy="85"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1294"
|
||||||
|
cx="115"
|
||||||
|
cy="95"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1296"
|
||||||
|
cx="115"
|
||||||
|
cy="105"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1298"
|
||||||
|
cx="110"
|
||||||
|
cy="115"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1300"
|
||||||
|
cx="90"
|
||||||
|
cy="115"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1302"
|
||||||
|
cx="85"
|
||||||
|
cy="105"
|
||||||
|
r="0.49999997" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1304"
|
||||||
|
cx="85"
|
||||||
|
cy="95"
|
||||||
|
r="0.49999997" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
304
Art/radial2.svg
Normal file
304
Art/radial2.svg
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="200mm"
|
||||||
|
height="200mm"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="radial2.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2"
|
||||||
|
inkscape:cx="375.25"
|
||||||
|
inkscape:cy="379.25"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1026"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid132"
|
||||||
|
spacingx="0.99999999"
|
||||||
|
spacingy="0.99999999" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="120"
|
||||||
|
y="90"
|
||||||
|
id="text2926"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2924"
|
||||||
|
style="stroke-width:1;font-size:7.05555556px"
|
||||||
|
x="120"
|
||||||
|
y="90" /></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="67.497505"
|
||||||
|
id="text3271"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3269"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="67.497505">Light the furnace</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="77.497505"
|
||||||
|
id="text3325"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3323"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="77.497505">Replenish Fuel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="87.497505"
|
||||||
|
id="text3329"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3327"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="87.497505">Bake chicken</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="97.497505"
|
||||||
|
id="text3333"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3331"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="97.497505">Cannibalism</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="107.49751"
|
||||||
|
id="text3271-9"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3269-1"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="107.49751">Light the furnace</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="117.49751"
|
||||||
|
id="text3325-2"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3323-7"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="117.49751">Replenish Fuel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="127.49751"
|
||||||
|
id="text3329-0"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3327-9"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="127.49751">Bake chicken</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="130"
|
||||||
|
y="137.4975"
|
||||||
|
id="text3333-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3331-6"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="130"
|
||||||
|
y="137.4975">Cannibalism</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="67.497505"
|
||||||
|
id="text3337"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3335"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="67.497505">Banana Bread</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="77.497505"
|
||||||
|
id="text3341"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3339"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="77.497505">Electric Eel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="87.497505"
|
||||||
|
id="text3345"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3343"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="87.497505">Engine Fire</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="97.497505"
|
||||||
|
id="text3349"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3347"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="97.497505">Radial Menu</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="107.49751"
|
||||||
|
id="text3337-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3335-6"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="107.49751">Banana Bread</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="117.49751"
|
||||||
|
id="text3341-7"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3339-5"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="117.49751">Electric Eel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="127.49751"
|
||||||
|
id="text3345-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3343-5"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="127.49751">Engine Fire</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="70"
|
||||||
|
y="137.4975"
|
||||||
|
id="text3349-6"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3347-2"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="70"
|
||||||
|
y="137.4975">Radial Menu</tspan></text>
|
||||||
|
<path
|
||||||
|
id="path4576"
|
||||||
|
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 82.000003,188 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 2,-4.00001 2,-4.00001 0,0 2.000004,2.89544 2.000004,4.00001 z"
|
||||||
|
sodipodi:nodetypes="ssscs" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1284"
|
||||||
|
cx="125"
|
||||||
|
cy="194.5"
|
||||||
|
r="0.49999997" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 135,160 h 30"
|
||||||
|
id="path1524" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 93.038085,64.999999 99.999992,99.999995 106.96191,65"
|
||||||
|
id="path1528"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 99.999995,99.999995 83.295555,75"
|
||||||
|
id="path1530"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 116.70446,75 99.999995,99.999995 77.550899,84.999992"
|
||||||
|
id="path1532"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 122.44906,84.99999 99.999995,99.999995 74.863225,95"
|
||||||
|
id="path1536"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 99.999995,99.999995 74.863247,105"
|
||||||
|
id="path1538" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 99.999992,99.999995 77.550885,114.99999"
|
||||||
|
id="path1544" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 83.29551,125 99.999995,99.999995"
|
||||||
|
id="path1546" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 93.038045,135 6.96195,-35.000005"
|
||||||
|
id="path1548" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 106.96198,135 99.999992,99.999995"
|
||||||
|
id="path1550" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 116.70444,125 99.999995,99.999995"
|
||||||
|
id="path1552" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 122.44909,114.99999 99.999995,99.999995"
|
||||||
|
id="path1554" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 125.13655,95 99.999995,99.999995 125.13674,105"
|
||||||
|
id="path1556"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
449
Art/radial3.svg
Normal file
449
Art/radial3.svg
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="200mm"
|
||||||
|
height="200mm"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="radial3.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2.8284271"
|
||||||
|
inkscape:cx="396.51013"
|
||||||
|
inkscape:cy="365.57421"
|
||||||
|
inkscape:window-width="1276"
|
||||||
|
inkscape:window-height="673"
|
||||||
|
inkscape:window-x="153"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid132"
|
||||||
|
spacingx="0.99999999"
|
||||||
|
spacingy="0.99999999" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7971"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7967"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7963"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7959"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7955"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7951"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7947"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7943"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7939"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7935"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7931"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7927"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7923"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7919"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7915"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
<inkscape:path-effect
|
||||||
|
effect="bspline"
|
||||||
|
id="path-effect7911"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
weight="33.333333"
|
||||||
|
steps="2"
|
||||||
|
helper_size="0"
|
||||||
|
apply_no_weight="true"
|
||||||
|
apply_with_weight="true"
|
||||||
|
only_selected="false" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05555556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:center;text-anchor:middle;fill:none;stroke:#707070;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="120"
|
||||||
|
y="90"
|
||||||
|
id="text2926"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2924"
|
||||||
|
style="stroke-width:1;font-size:7.05555556px"
|
||||||
|
x="120"
|
||||||
|
y="90" /></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="67.497505"
|
||||||
|
id="text3271"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3269"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="67.497505">Light the furnace</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="77.497505"
|
||||||
|
id="text3325"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3323"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="77.497505">Replenish Fuel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="87.497505"
|
||||||
|
id="text3329"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3327"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="87.497505">Bake chicken</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="97.497505"
|
||||||
|
id="text3333"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3331"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="97.497505">Cannibalism</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="107.49751"
|
||||||
|
id="text3271-9"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3269-1"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="107.49751">Light the furnace</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="117.49751"
|
||||||
|
id="text3325-2"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3323-7"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="117.49751">Replenish Fuel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="127.49751"
|
||||||
|
id="text3329-0"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3327-9"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="127.49751">Bake chicken</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="105"
|
||||||
|
y="137.4975"
|
||||||
|
id="text3333-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3331-6"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="105"
|
||||||
|
y="137.4975">Cannibalism</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="67.497505"
|
||||||
|
id="text3337"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3335"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="67.497505">Banana Bread</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="77.497505"
|
||||||
|
id="text3341"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3339"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="77.497505">Electric Eel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="87.497505"
|
||||||
|
id="text3345"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3343"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="87.497505">Engine Fire</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="97.497505"
|
||||||
|
id="text3349"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3347"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="97.497505">Radial Menu</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="107.49751"
|
||||||
|
id="text3337-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3335-6"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="107.49751">Banana Bread</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="117.49751"
|
||||||
|
id="text3341-7"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3339-5"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="117.49751">Electric Eel</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="127.49751"
|
||||||
|
id="text3345-3"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3343-5"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="127.49751">Engine Fire</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-weight:bold;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:end;text-anchor:end;fill:#666666;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
x="65"
|
||||||
|
y="137.4975"
|
||||||
|
id="text3349-6"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3347-2"
|
||||||
|
style="stroke-width:1"
|
||||||
|
x="65"
|
||||||
|
y="137.4975">Radial Menu</tspan></text>
|
||||||
|
<path
|
||||||
|
id="path4576"
|
||||||
|
style="fill:#662066;stroke:#707070;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
d="m 82.000003,188 c 0,1.10457 -0.89543,2 -2.000004,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 2,-4.00001 2,-4.00001 0,0 2.000004,2.89544 2.000004,4.00001 z"
|
||||||
|
sodipodi:nodetypes="ssscs" />
|
||||||
|
<circle
|
||||||
|
style="fill:#662066;fill-opacity:1;stroke:none;stroke-width:0.3;stroke-linecap:round"
|
||||||
|
id="circle1284"
|
||||||
|
cx="125"
|
||||||
|
cy="194.5"
|
||||||
|
r="0.49999997" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#707070;stroke-width:1;stroke-linecap:round;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 135,160 h 30"
|
||||||
|
id="path1524" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:none;stroke:#700726;stroke-width:1;stroke-linecap:round"
|
||||||
|
id="path9423"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="8"
|
||||||
|
sodipodi:cx="84.999992"
|
||||||
|
sodipodi:cy="50"
|
||||||
|
sodipodi:r1="10"
|
||||||
|
sodipodi:r2="10"
|
||||||
|
sodipodi:arg1="1.5707963"
|
||||||
|
sodipodi:arg2="1.9634954"
|
||||||
|
inkscape:rounded="0"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 84.999993,60 81.173158,59.238795 77.928925,57.071068 75.761197,53.826834 74.999992,50 75.761197,46.173166 77.928924,42.928932 81.173158,40.761205 84.999992,40 l 3.826835,0.761205 3.244233,2.167727 2.167728,3.244234 L 94.999992,50 l -0.761204,3.826834 -2.167728,3.244234 -3.244233,2.167727 z"
|
||||||
|
inkscape:transform-center-x="1.0036851e-06"
|
||||||
|
transform="rotate(11.25,-168.82926,75.000033)" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#700726;stroke-width:1;stroke-linecap:round"
|
||||||
|
d="M 64.999999,64.999999 H 105 V 74.999997 84.999998 94.999997 105 v 10 10 10 H 64.999999 V 125 115 105 94.999997 84.999998 74.999997 64.999999"
|
||||||
|
id="path9425" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -84,3 +84,16 @@ DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.Defaul
|
|||||||
-ConsoleKeys=Tilde
|
-ConsoleKeys=Tilde
|
||||||
+ConsoleKeys=Tilde
|
+ConsoleKeys=Tilde
|
||||||
|
|
||||||
|
[InputPlatformSettings_Linux InputPlatformSettings]
|
||||||
|
MaxPlatformUserCount=8
|
||||||
|
input.DeviceMappingPolicy=1
|
||||||
|
MaxTriggerFeedbackPosition=8
|
||||||
|
MaxTriggerFeedbackStrength=8
|
||||||
|
MaxTriggerVibrationTriggerPosition=9
|
||||||
|
MaxTriggerVibrationFrequency=255
|
||||||
|
MaxTriggerVibrationAmplitude=8
|
||||||
|
+HardwareDevices=(InputClassName="",HardwareDeviceIdentifier="",PrimaryDeviceType=Unspecified,SupportedFeaturesMask=0)
|
||||||
|
+HardwareDevices=(InputClassName="DefaultKeyboardAndMouse",HardwareDeviceIdentifier="KBM",PrimaryDeviceType=KeyboardAndMouse,SupportedFeaturesMask=3)
|
||||||
|
+HardwareDevices=(InputClassName="DefaultGamepad",HardwareDeviceIdentifier="Gamepad",PrimaryDeviceType=Gamepad,SupportedFeaturesMask=4)
|
||||||
|
+HardwareDevices=(InputClassName="DefaultMobileTouch",HardwareDeviceIdentifier="MobileTouch",PrimaryDeviceType=Touch,SupportedFeaturesMask=8)
|
||||||
|
|
||||||
|
|||||||
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/lxGameMode.uasset
LFS
BIN
Content/Luprex/lxGameMode.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Testing/BP_Test.uasset
LFS
BIN
Content/Testing/BP_Test.uasset
LFS
Binary file not shown.
BIN
Content/Testing/WB_Test.uasset
LFS
BIN
Content/Testing/WB_Test.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/WB_Menu.uasset
LFS
BIN
Content/Widgets/WB_Menu.uasset
LFS
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
Binary file not shown.
@@ -1,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.
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
|
|
||||||
* UE Wingman rename functions.
|
|
||||||
* ue Wingman 'structprop' doesn't work for UWingXXXRef types, or for Widget slots. It needs to be implemented on top of getdetails.
|
|
||||||
* In the console, do not allow multi-line lua expressions unless it's something that reasonably should be multi-line, like a function definition or an if-statement.
|
|
||||||
|
|
||||||
* Keyboard Event Handling
|
|
||||||
|
|
||||||
* Menus
|
* Add a slash-command to reload lua source code.
|
||||||
|
|
||||||
* Skeletal Mesh Tangible
|
* Skeletal Mesh Tangible
|
||||||
|
|
||||||
* Implement Interactive Temporary Variables
|
* Implement Interactive Temporary Variables
|
||||||
|
|
||||||
* A better text console
|
|
||||||
|
|
||||||
* Get rid of 3x3 Gridpanel stuff
|
|
||||||
|
|
||||||
* Object-Oriented Lua Support
|
* Object-Oriented Lua Support
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
286
EnginePatches/EnginePatch-5.7.4
Normal file
286
EnginePatches/EnginePatch-5.7.4
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-05-08 14:11:02.262757499 -0400
|
||||||
|
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-05-05 15:36:35.232152395 -0400
|
||||||
|
@@ -149,7 +149,7 @@
|
||||||
|
FString SolutionDir = GetSolutionPath();
|
||||||
|
TArray<FString> Args;
|
||||||
|
Args.Add(MakePath(SolutionDir));
|
||||||
|
- Args.Add(TEXT("-g ") + MakePath(FullPath) + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber));
|
||||||
|
+ Args.Add(TEXT("-g ") + MakePath(FullPath + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber)));
|
||||||
|
return Launch(Args);
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp.orig 2026-05-08 14:11:02.370759731 -0400
|
||||||
|
+++ Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp 2026-05-05 15:36:35.232353795 -0400
|
||||||
|
@@ -557,7 +557,7 @@
|
||||||
|
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
|
||||||
|
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
|
||||||
|
|
||||||
|
-#if PLATFORM_WINDOWS
|
||||||
|
+#if PLATFORM_WINDOWS || PLATFORM_LINUX
|
||||||
|
FString SourceFileName;
|
||||||
|
uint32 SourceLineNumber = 1;
|
||||||
|
uint32 SourceColumnNumber = 0;
|
||||||
|
@@ -716,8 +716,8 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogSelectionDetails, Warning, TEXT("NavigateToFunctionSource: Unable to look up symbol: %s in module:%s"), *FunctionSymbolName, *FunctionModuleName);
|
||||||
|
-
|
||||||
|
-#endif // PLATFORM_WINDOWS
|
||||||
|
+
|
||||||
|
+#endif // PLATFORM_WINDOWS || PLATFORM_LINUX
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp.orig 2026-05-08 14:11:02.477761942 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp 2026-05-05 15:36:35.232635087 -0400
|
||||||
|
@@ -317,6 +317,9 @@
|
||||||
|
// Furthermore SDL hides the mouse which we prevent by setting SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE, "1"); // When relative mouse mode is active, don't hide cursor.
|
||||||
|
|
||||||
|
+ // Unreal does its own dynamic capturing, we don't need SDL to do it.
|
||||||
|
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
|
||||||
|
+
|
||||||
|
// If we're rendering offscreen, use the "dummy" SDL video driver
|
||||||
|
if (FParse::Param(FCommandLine::Get(), TEXT("RenderOffScreen")) && !getenv("SDL_VIDEODRIVER"))
|
||||||
|
{
|
||||||
|
--- Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp.orig 2026-05-08 14:11:02.584764153 -0400
|
||||||
|
+++ Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp 2026-05-05 15:36:35.232750204 -0400
|
||||||
|
@@ -15,6 +15,7 @@
|
||||||
|
#include "HAL/ExceptionHandling.h"
|
||||||
|
#include "HAL/PlatformProcess.h"
|
||||||
|
#include "HAL/PlatformTime.h"
|
||||||
|
+#include "Modules/ModuleManager.h"
|
||||||
|
#include "AutoRTFM.h"
|
||||||
|
|
||||||
|
#include <link.h>
|
||||||
|
@@ -1063,3 +1064,69 @@
|
||||||
|
}
|
||||||
|
ReportLock.Unlock();
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+bool FUnixPlatformStackWalk::GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber)
|
||||||
|
+{
|
||||||
|
+ // Find the .so path for this module.
|
||||||
|
+ FString ModulePath;
|
||||||
|
+ TArray<FModuleStatus> AllModules;
|
||||||
|
+ FModuleManager::Get().QueryModules(AllModules);
|
||||||
|
+ for (const FModuleStatus& Status : AllModules)
|
||||||
|
+ {
|
||||||
|
+ if (FPaths::GetBaseFilename(Status.FilePath) == FunctionModuleName)
|
||||||
|
+ {
|
||||||
|
+ ModulePath = Status.FilePath;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ if (ModulePath.IsEmpty())
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Debug symbols are in a separate .debug file alongside the .so.
|
||||||
|
+ FString DebugPath = FPaths::ChangeExtension(ModulePath, TEXT("debug"));
|
||||||
|
+ if (!FPaths::FileExists(DebugPath))
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Use lldb to look up the source file and line number.
|
||||||
|
+ // Run: lldb -b -o "image lookup -v -n ClassName::FuncName" <debug_file>
|
||||||
|
+ FString LldbParams = FString::Printf(TEXT("-b -o \"image lookup -v -n %s\" \"%s\""), *FunctionSymbolName, *DebugPath);
|
||||||
|
+ int32 ReturnCode = 0;
|
||||||
|
+ FString AllOutput;
|
||||||
|
+ FString Errors;
|
||||||
|
+ FPlatformProcess::ExecProcess(TEXT("/usr/bin/lldb"), *LldbParams, &ReturnCode, &AllOutput, &Errors);
|
||||||
|
+ if (ReturnCode != 0)
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Parse the LineEntry from lldb verbose output.
|
||||||
|
+ // Format: "LineEntry: [0x...-0x...): /path/to/file.cpp:132"
|
||||||
|
+ TArray<FString> Lines;
|
||||||
|
+ AllOutput.ParseIntoArrayLines(Lines);
|
||||||
|
+ for (const FString& Line : Lines)
|
||||||
|
+ {
|
||||||
|
+ FString Trimmed = Line.TrimStartAndEnd();
|
||||||
|
+ if (!Trimmed.StartsWith(TEXT("LineEntry:")))
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
+ int32 ParenIndex = Trimmed.Find(TEXT("): "));
|
||||||
|
+ if (ParenIndex == INDEX_NONE)
|
||||||
|
+ continue;
|
||||||
|
+ FString FileAndLine = Trimmed.Mid(ParenIndex + 3);
|
||||||
|
+
|
||||||
|
+ int32 ColonIndex;
|
||||||
|
+ if (!FileAndLine.FindLastChar(TCHAR(':'), ColonIndex))
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
+ OutPathname = FileAndLine.Left(ColonIndex);
|
||||||
|
+ OutLineNumber = FCString::Atoi(*FileAndLine.Mid(ColonIndex + 1));
|
||||||
|
+ OutColumnNumber = 0;
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return false;
|
||||||
|
+}
|
||||||
|
--- Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h.orig 2026-05-08 14:11:02.692766385 -0400
|
||||||
|
+++ Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h 2026-05-05 15:36:35.232861435 -0400
|
||||||
|
@@ -24,6 +24,8 @@
|
||||||
|
static CORE_API void ThreadStackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, int32 IgnoreCount, uint32 ThreadId);
|
||||||
|
static CORE_API int32 GetProcessModuleCount();
|
||||||
|
static CORE_API int32 GetProcessModuleSignatures(FStackWalkModuleInfo *ModuleSignatures, const int32 ModuleSignaturesSize);
|
||||||
|
+
|
||||||
|
+ static CORE_API bool GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef FUnixPlatformStackWalk FPlatformStackWalk;
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h.orig 2026-05-08 14:11:02.802768658 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h 2026-05-08 14:09:14.778161399 -0400
|
||||||
|
@@ -265,6 +265,12 @@
|
||||||
|
/** The input device Id of the controller that can be used to find the matching ULocalPlayer */
|
||||||
|
FInputDeviceId DeviceId;
|
||||||
|
|
||||||
|
+ /** SDL gamepad type string (e.g. "ps4", "xboxone"), used as HardwareDeviceIdentifier in FInputDeviceScope */
|
||||||
|
+ FName GamepadType;
|
||||||
|
+
|
||||||
|
+ /** SDL gamepad name (human-readable device name), used as InputDeviceName in FInputDeviceScope */
|
||||||
|
+ FName GamepadName;
|
||||||
|
+
|
||||||
|
/** Store axis values from events here to be handled once per frame. */
|
||||||
|
TMap<FGamepadKeyNames::Type, float> AxisEvents;
|
||||||
|
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp.orig 2026-05-08 14:11:02.910770890 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp 2026-05-08 14:09:14.778345206 -0400
|
||||||
|
@@ -13,6 +13,13 @@
|
||||||
|
#include "IHapticDevice.h"
|
||||||
|
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
|
||||||
|
|
||||||
|
+namespace UE::LinuxInput
|
||||||
|
+{
|
||||||
|
+ static const FName InputClassName = TEXT("LinuxApplication");
|
||||||
|
+ static const FString KBMInputHardwareName = TEXT("KBM");
|
||||||
|
+ static const FString TouchInputHardwareName = TEXT("MobileTouch");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
//
|
||||||
|
// GameController thresholds
|
||||||
|
//
|
||||||
|
@@ -320,6 +327,7 @@
|
||||||
|
{
|
||||||
|
case SDL_EVENT_KEY_DOWN:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
const SDL_KeyboardEvent &KeyEvent = Event.key;
|
||||||
|
SDL_Keycode KeySym = KeyEvent.key;
|
||||||
|
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
|
||||||
|
@@ -342,6 +350,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_KEY_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
const SDL_KeyboardEvent &KeyEvent = Event.key;
|
||||||
|
const SDL_Keycode KeySym = KeyEvent.key;
|
||||||
|
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
|
||||||
|
@@ -352,6 +361,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_TEXT_INPUT:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
// Slate now gets all its text from here, I hope.
|
||||||
|
const bool bIsRepeated = false; //Event.key.repeat != 0;
|
||||||
|
const FString TextStr(UTF8_TO_TCHAR(Event.text.text));
|
||||||
|
@@ -363,6 +373,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseMotionEvent motionEvent = Event.motion;
|
||||||
|
FLinuxCursor *LinuxCursor = (FLinuxCursor*)Cursor.Get();
|
||||||
|
LinuxCursor->InvalidateCaches();
|
||||||
|
@@ -406,6 +417,7 @@
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseButtonEvent buttonEvent = Event.button;
|
||||||
|
|
||||||
|
EMouseButtons::Type button;
|
||||||
|
@@ -484,6 +496,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_WHEEL:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseWheelEvent *WheelEvent = &Event.wheel;
|
||||||
|
float Amount = (float)WheelEvent->y * fMouseWheelScrollAccel;
|
||||||
|
|
||||||
|
@@ -515,6 +528,7 @@
|
||||||
|
|
||||||
|
SDLControllerState &ControllerState = ControllerStates[caxisEvent.which];
|
||||||
|
FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerState.DeviceId);
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ControllerState.GamepadName, ControllerState.DeviceId.GetId(), ControllerState.GamepadType.ToString());
|
||||||
|
|
||||||
|
switch (caxisEvent.axis)
|
||||||
|
{
|
||||||
|
@@ -740,15 +754,17 @@
|
||||||
|
|
||||||
|
if (Button != FGamepadKeyNames::Invalid)
|
||||||
|
{
|
||||||
|
- FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerStates[cbuttonEvent.which].DeviceId);
|
||||||
|
+ SDLControllerState& ButtonControllerState = ControllerStates[cbuttonEvent.which];
|
||||||
|
+ FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ButtonControllerState.DeviceId);
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ButtonControllerState.GamepadName, ButtonControllerState.DeviceId.GetId(), ButtonControllerState.GamepadType.ToString());
|
||||||
|
|
||||||
|
if(cbuttonEvent.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN)
|
||||||
|
{
|
||||||
|
- MessageHandler->OnControllerButtonPressed(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
|
||||||
|
+ MessageHandler->OnControllerButtonPressed(Button, UserId, ButtonControllerState.DeviceId, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
- MessageHandler->OnControllerButtonReleased(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
|
||||||
|
+ MessageHandler->OnControllerButtonReleased(Button, UserId, ButtonControllerState.DeviceId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1018,6 +1034,7 @@
|
||||||
|
|
||||||
|
case SDL_EVENT_FINGER_DOWN:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is down at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
|
||||||
|
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
@@ -1053,6 +1070,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_FINGER_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is up at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
|
||||||
|
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
@@ -1087,6 +1105,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_FINGER_MOTION:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
if (LIKELY(!bWindowlessEvent))
|
||||||
|
{
|
||||||
|
@@ -1196,6 +1215,7 @@
|
||||||
|
IPlatformInputDeviceMapper& Mapper = IPlatformInputDeviceMapper::Get();
|
||||||
|
for(auto ControllerIt = ControllerStates.CreateIterator(); ControllerIt; ++ControllerIt)
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ControllerIt.Value().GamepadName, ControllerIt.Value().DeviceId.GetId(), ControllerIt.Value().GamepadType.ToString());
|
||||||
|
for(auto Event = ControllerIt.Value().AxisEvents.CreateConstIterator(); Event; ++Event)
|
||||||
|
{
|
||||||
|
FPlatformUserId UserId = Mapper.GetUserForInputDevice(ControllerIt.Value().DeviceId);
|
||||||
|
@@ -2075,6 +2095,9 @@
|
||||||
|
UE_LOG(LogLinux, Verbose, TEXT("Adding controller %i '%s'"), FirstUnusedIndex, UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
|
||||||
|
auto& ControllerState = ControllerStates.Add(Id);
|
||||||
|
ControllerState.Controller = Controller;
|
||||||
|
+ ControllerState.GamepadName = FName(UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
|
||||||
|
+ const char* GamepadTypeStr = SDL_GetGamepadStringForType(SDL_GetGamepadType(Controller));
|
||||||
|
+ ControllerState.GamepadType = FName(GamepadTypeStr ? UTF8_TO_TCHAR(GamepadTypeStr) : TEXT("unknown"));
|
||||||
|
|
||||||
|
FPlatformUserId UserId = FPlatformUserId::CreateFromInternalId(FirstUnusedIndex);
|
||||||
|
DeviceMapper.RemapControllerIdToPlatformUserAndDevice(FirstUnusedIndex, UserId, ControllerState.DeviceId);
|
||||||
47
EnginePatches/Old-Patches.md
Normal file
47
EnginePatches/Old-Patches.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Old Patches
|
||||||
|
|
||||||
|
Patches that were once part of `EnginePatch` but are no longer needed.
|
||||||
|
Kept here for reference in case the underlying issue resurfaces.
|
||||||
|
|
||||||
|
## LinuxWindow.cpp — force override_redirect for borderless child windows
|
||||||
|
|
||||||
|
Removed when the engine was upgraded to UE 5.7, which uses SDL3. SDL3 has
|
||||||
|
better Wayland support, so this workaround is believed to be unnecessary.
|
||||||
|
The original purpose was to make Unreal play better with Wayland: under
|
||||||
|
XWayland, non-override-redirect popup windows don't receive input events
|
||||||
|
from the compositor. SDL already sets override_redirect for tooltips and
|
||||||
|
popup menus, but other borderless child windows (notification popups,
|
||||||
|
dialogs) also needed it, so we temporarily enabled
|
||||||
|
`SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT` around the `SDL_CreateWindow` call.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp.orig 2026-04-12 22:58:34.538334467 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp 2026-04-12 22:48:15.848291098 -0400
|
||||||
|
@@ -235,7 +235,26 @@
|
||||||
|
|
||||||
|
// The SDL window doesn't need to be reshaped.
|
||||||
|
// the size of the window you input is the sizeof the client.
|
||||||
|
+
|
||||||
|
+ // Under XWayland, non-override-redirect popup windows don't receive input
|
||||||
|
+ // events from the compositor. SDL already sets override_redirect for
|
||||||
|
+ // tooltips and popup menus, but other borderless child windows (like
|
||||||
|
+ // notification popups and dialogs) also need it. We temporarily enable
|
||||||
|
+ // the SDL hint to force override_redirect for these windows.
|
||||||
|
+ bool bForceOverrideRedirect = !Definition->HasOSWindowBorder
|
||||||
|
+ && InParent.IsValid()
|
||||||
|
+ && !(WindowStyle & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU));
|
||||||
|
+ if (bForceOverrideRedirect)
|
||||||
|
+ {
|
||||||
|
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "1");
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
HWnd = SDL_CreateWindow( TCHAR_TO_ANSI( *Definition->Title ), X, Y, ClientWidth, ClientHeight, WindowStyle );
|
||||||
|
+
|
||||||
|
+ if (bForceOverrideRedirect)
|
||||||
|
+ {
|
||||||
|
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "0");
|
||||||
|
+ }
|
||||||
|
// produce a helpful message for common driver errors
|
||||||
|
if (HWnd == nullptr)
|
||||||
|
{
|
||||||
|
```
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"files.watcherExclude": {
|
"files.watcherExclude": {
|
||||||
"[UNREALENGINE]/Engine/**": true,
|
"[UNREALENGINE]/Engine/**": true,
|
||||||
"[UNREALENGINE]/Samples/**": true,
|
"[UNREALENGINE]/Samples/**": true,
|
||||||
"[UNREALENGINE]/Templates/**": true
|
"[UNREALENGINE]/Templates/**": true,
|
||||||
|
"**/.*": true
|
||||||
},
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"**/include/**": "cpp",
|
"**/include/**": "cpp",
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"editor.acceptSuggestionOnEnter": "off",
|
"editor.acceptSuggestionOnEnter": "off",
|
||||||
"C_Cpp.intelliSenseEngine": "disabled",
|
"C_Cpp.intelliSenseEngine": "disabled",
|
||||||
"clangd.path": "/usr/bin/clangd-15",
|
"clangd.path": "/usr/bin/clangd-16",
|
||||||
"clangd.arguments": [
|
"clangd.arguments": [
|
||||||
"--log=verbose",
|
"--log=verbose",
|
||||||
"--query-driver=/usr/bin/g++",
|
"--query-driver=/usr/bin/g++",
|
||||||
@@ -50,6 +51,9 @@
|
|||||||
],
|
],
|
||||||
"C_Cpp.autocomplete": "disabled",
|
"C_Cpp.autocomplete": "disabled",
|
||||||
"search.useIgnoreFiles": true,
|
"search.useIgnoreFiles": true,
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"[UNREALENGINE]/**": true
|
||||||
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/Intermediate": true,
|
"**/Intermediate": true,
|
||||||
"**/Saved": true,
|
"**/Saved": true,
|
||||||
@@ -127,7 +131,7 @@
|
|||||||
"settings set target.inline-breakpoint-strategy always",
|
"settings set target.inline-breakpoint-strategy always",
|
||||||
"settings set target.prefer-dynamic-value no-run-target",
|
"settings set target.prefer-dynamic-value no-run-target",
|
||||||
"process handle SIGTRAP --notify false --pass false --stop false",
|
"process handle SIGTRAP --notify false --pass false --stop false",
|
||||||
"target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\""
|
"target stop-hook add --one-liner \"p FUnixPlatformMisc::UngrabAllInput()\""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials.
|
|||||||
|
|
||||||
## How Does it Work?
|
## 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
|
Editor. You can type commands, and the plugin in the editor
|
||||||
will execute them. You can actually type commands directly
|
will execute them.
|
||||||
from the command-line. Here's an example of what you might
|
|
||||||
see:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ 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
|
node K2Node_Event_0: Event BeginPlay
|
||||||
output-pins OutputDelegate
|
output-pins OutputDelegate
|
||||||
@@ -32,33 +30,30 @@ see:
|
|||||||
output-pins OutputDelegate, DeltaSeconds
|
output-pins OutputDelegate, DeltaSeconds
|
||||||
```
|
```
|
||||||
|
|
||||||
There are tons of commands built in: Graph_Dump,
|
The ue-wingman command has tons of subcommands: Graph_Dump,
|
||||||
GraphNode_Add, GraphPin_Connect,
|
GraphNode_Add, GraphPin_Connect, BlueprintComponent_Add,
|
||||||
BlueprintComponent_Add, Widget_Add, and so forth.
|
Widget_Add, and so forth. Using these commands, it's
|
||||||
Using these commands, it's possible to examine and modify
|
possible to examine and modify blueprints, widgets, and
|
||||||
blueprints, widgets, and materials.
|
materials.
|
||||||
|
|
||||||
But, of course, these commands aren't really intended for humans.
|
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
|
They're intended for an AI agent.
|
||||||
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."
|
|
||||||
|
|
||||||
## 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
|
them are, shall we say, not carefully engineered. I'm a
|
||||||
reasonably skilled software engineer and I've designed
|
reasonably skilled software engineer and I've designed
|
||||||
this plugin to be robust and capable of sustained development.
|
this plugin to be robust and capable of sustained development.
|
||||||
|
|
||||||
This MCP is also designed to be as broadly general as
|
This plugin is also designed to be as broadly general as
|
||||||
possible. I've seen MCPs that claim "can create 22 different
|
possible. I've seen plugins that claim "can create 22 different
|
||||||
kinds of graph nodes!" This makes me ask: why not just
|
kinds of graph nodes!" This makes me ask: why not just
|
||||||
provide the *entire catalog* of all possible graph nodes?
|
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*
|
expression properties!" Why not provide access to *all*
|
||||||
editable material expression properties? I've tried to make
|
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.
|
limits as possible.
|
||||||
|
|
||||||
Some of the MCPs out there expose the entire Unreal API to
|
Some of the MCPs out there expose the entire Unreal API to
|
||||||
@@ -76,15 +71,16 @@ commands.
|
|||||||
|
|
||||||
## Installation
|
## 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 Unreal Plugin, which does 99% of the work.
|
||||||
|
|
||||||
* The python program "ue-wingman.py" which a human can
|
* The python program "ue-wingman.py"
|
||||||
use to send commands to the plugin.
|
|
||||||
|
The python program is actually less than 100 lines of code:
|
||||||
* The python program "ue-wingman-mcp.py", which an AI
|
all it does is package up its command line arguments, send
|
||||||
can use to send commands to the plugin.
|
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
|
If you build Unreal from source, the best way to install the
|
||||||
plugin is to drop the entire UEWingman source folder into
|
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
|
To install the human version, ue-wingman.py, just drop it into
|
||||||
a folder on your PATH.
|
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"
|
## The "User Manual"
|
||||||
|
|
||||||
You might be interested in seeing the "user manual" for the
|
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.
|
Of course, you're not the intended user: your AI agent is.
|
||||||
When the AI agent starts up ue-wingman-mcp, it is
|
You should put a note into your agent's system prompt to
|
||||||
automatically told to read the user manual. From there, the
|
let it know about the ue-wingman.py command, and to let
|
||||||
User Manual says, among other things, that the AI agent can
|
it know that it can type ue-wingman.py Documentation_Manual.
|
||||||
get a listing of built-in commands. You can see that too:
|
This in turn will tell your agent about this command:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ue-wingman.py Documentation_Commands
|
$ ue-wingman.py Documentation_Commands
|
||||||
```
|
```
|
||||||
|
|
||||||
With these two commands at your disposal, you'll have a better
|
Using these commands, you can learn more about what this
|
||||||
understanding of what exactly your AI agent is doing with this
|
plugin can do.
|
||||||
plugin, and how it all works.
|
|
||||||
|
|
||||||
## Fun things to Try
|
## Fun things to Try
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
|
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
|
||||||
FString Asset;
|
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;
|
bool Force = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
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;
|
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;
|
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;
|
int32 Limit = 50;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -32,16 +32,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
|
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
|
||||||
FString GraphType;
|
FString GraphType;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
|
||||||
FString InputVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
|
|
||||||
FString OutputVariables;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -79,8 +78,7 @@ public:
|
|||||||
|
|
||||||
// Parse and validate variables before making changes
|
// Parse and validate variables before making changes
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
|
|
||||||
|
|
||||||
// Create the Graph
|
// Create the Graph
|
||||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,
|
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
|
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
|
||||||
FString Interface;
|
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;
|
bool PreserveFunctions = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the 'Class' property.
|
// Set the 'Class' property.
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory);
|
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory, true);
|
||||||
FWingProperty::Remove(Props, TEXT("BlueprintType"));
|
FWingProperty::Remove(Props, TEXT("BlueprintType"));
|
||||||
if (Props.Num() != 1)
|
if (Props.Num() != 1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,5 +37,6 @@ public:
|
|||||||
if (!P) return;
|
if (!P) return;
|
||||||
|
|
||||||
WingOut::Stdout.Print(P->GetText());
|
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()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
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
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
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
|
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()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
|
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
|
||||||
FString Query;
|
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;
|
bool Verbose = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -12,41 +12,21 @@ class UWing_Documentation_Manual : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual"))
|
|
||||||
FString Section;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
TStringBuilder<128> Docs;
|
UWingServer::AddHandler(this, TEXT("Print the entire manual."));
|
||||||
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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
TSet<FName> Sections = WingManual::GetSections();
|
UWingManualSections::FetcherPaths();
|
||||||
if (Section.IsEmpty())
|
UWingManualSections::ExpressingTypes();
|
||||||
{
|
UWingManualSections::VariableDeclarations();
|
||||||
UWingManualSections::FetcherPaths();
|
UWingManualSections::EscapeSequencesInFNames();
|
||||||
UWingManualSections::ExpressingTypes();
|
UWingManualSections::MaterialEditing();
|
||||||
UWingManualSections::VariableDeclarations();
|
UWingManualSections::NodeContextMenus();
|
||||||
UWingManualSections::EscapeSequencesInFNames();
|
UWingManualSections::VariableGettersAndSetters();
|
||||||
UWingManualSections::MaterialEditing();
|
UWingManualSections::BestPerformance();
|
||||||
UWingManualSections::ImportantCommands();
|
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"))
|
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
|
||||||
FString Dispatcher;
|
FString Dispatcher;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
|
||||||
FString InputVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -49,7 +52,7 @@ public:
|
|||||||
|
|
||||||
// Parse the arguments.
|
// Parse the arguments.
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
|
||||||
|
|
||||||
// Add the delegate variable
|
// Add the delegate variable
|
||||||
FEdGraphPinType DelegateType;
|
FEdGraphPinType DelegateType;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include "WingServer.h"
|
#include "WingServer.h"
|
||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
#include "WingFetcher.h"
|
#include "WingFetcher.h"
|
||||||
#include "WingProperty.h"
|
|
||||||
#include "WingUtils.h"
|
#include "WingUtils.h"
|
||||||
#include "WingGraphActions.h"
|
#include "WingGraphActions.h"
|
||||||
#include "WingGraphExport.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()
|
UCLASS()
|
||||||
class UWing_GraphNode_Add : public UWingHandler
|
class UWing_GraphNode_Add : public UWingHandler
|
||||||
{
|
{
|
||||||
@@ -43,8 +24,14 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of {Type, posX, posY} objects. Use GraphNode_SearchTypes to find types."))
|
UPROPERTY(EditAnywhere, meta=(Description="Node type, from GraphNode_SearchTypes"))
|
||||||
FWingJsonArray Nodes;
|
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
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
@@ -58,38 +45,19 @@ public:
|
|||||||
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
|
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
|
||||||
if (!TargetGraph) return;
|
if (!TargetGraph) return;
|
||||||
|
|
||||||
int32 SuccessCount = 0;
|
|
||||||
int32 TotalCount = Nodes.Array.Num();
|
|
||||||
FWingGraphActions GraphActions(TargetGraph);
|
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.
|
UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(PosX, PosY));
|
||||||
TArray<FSpawnNodeEntry> Entries;
|
if (NewNode)
|
||||||
FSpawnNodeEntry Entry;
|
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
|
|
||||||
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
|
|
||||||
{
|
{
|
||||||
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;
|
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Type);
|
||||||
TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.Type, 2, true);
|
WingGraphExport Export(NewNode, false, true);
|
||||||
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Entry.Type, WingOut::Stdout)) return;
|
WingOut::Stdout.Print(Export.GetOutput());
|
||||||
Entry.Action = Results[0];
|
return;
|
||||||
Entries.Add(Entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute all.
|
WingOut::Stdout.Printf(TEXT("Failed: %s\n"), *Type);
|
||||||
for (const FSpawnNodeEntry &Entry : Entries)
|
|
||||||
{
|
|
||||||
UEdGraphNode* NewNode = Entry.Action->Execute(FVector2D(Entry.PosX, Entry.PosY));
|
|
||||||
if (NewNode)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Entry.Type);
|
|
||||||
WingGraphExport Export(NewNode, false, true);
|
|
||||||
WingOut::Stdout.Print(Export.GetOutput());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT("Failed: %s\n\n"), *Entry.Type);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
|
||||||
FString 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;
|
bool Details = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ class UWing_GraphNode_SearchTypes : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain * wildcards"))
|
|
||||||
FWingJsonArray Queries;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
|
|
||||||
int32 MaxResults = 50;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString 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
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
@@ -40,22 +40,8 @@ public:
|
|||||||
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
|
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
|
||||||
if (!TargetGraph) return;
|
if (!TargetGraph) return;
|
||||||
|
|
||||||
// Validate all entries are strings before running any searches.
|
|
||||||
TArray<FString> QueryStrings;
|
|
||||||
QueryStrings.Reserve(Queries.Array.Num());
|
|
||||||
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
|
|
||||||
{
|
|
||||||
FString QueryStr;
|
|
||||||
if (!QueryVal->TryGetString(QueryStr))
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QueryStrings.Add(QueryStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
FWingGraphActions GraphActions(TargetGraph);
|
FWingGraphActions GraphActions(TargetGraph);
|
||||||
for (const FString& Query : QueryStrings)
|
for (const FString& Query : Queries.Argv)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||||
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
|
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
|
||||||
|
|||||||
@@ -4,36 +4,15 @@
|
|||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
#include "WingServer.h"
|
#include "WingServer.h"
|
||||||
#include "WingFetcher.h"
|
#include "WingFetcher.h"
|
||||||
#include "WingProperty.h"
|
|
||||||
#include "WingUtils.h"
|
#include "WingUtils.h"
|
||||||
#include "EdGraph/EdGraphPin.h"
|
#include "EdGraph/EdGraphPin.h"
|
||||||
#include "EdGraphSchema_K2.h"
|
#include "EdGraphSchema_K2.h"
|
||||||
#include "MaterialGraph/MaterialGraphSchema.h"
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
#include "GraphNode_SetDefaults.generated.h"
|
#include "GraphNode_SetDefault.generated.h"
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FSetNodeDefaultEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Node;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Value;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class UWing_GraphNode_SetDefaults : public UWingHandler
|
class UWing_GraphNode_SetDefault : public UWingHandler
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -41,8 +20,14 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of {node, name, value} objects"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
|
||||||
FWingJsonArray Pins;
|
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
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
@@ -53,15 +38,15 @@ public:
|
|||||||
// K2 graphs: set pin default values.
|
// 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);
|
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;
|
if (!PinRef) return;
|
||||||
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
|
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
|
||||||
if (!Pin) return;
|
if (!Pin) return;
|
||||||
|
|
||||||
UEdGraphNode* Node = Pin->GetOwningNode();
|
UEdGraphNode* FoundNode = Pin->GetOwningNode();
|
||||||
|
|
||||||
if (Pin->Direction != EGPD_Input)
|
if (Pin->Direction != EGPD_Input)
|
||||||
{
|
{
|
||||||
@@ -72,34 +57,34 @@ public:
|
|||||||
FString UseDefaultValue;
|
FString UseDefaultValue;
|
||||||
TObjectPtr<UObject> UseDefaultObject = nullptr;
|
TObjectPtr<UObject> UseDefaultObject = nullptr;
|
||||||
FText UseDefaultText;
|
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);
|
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
|
||||||
if (!Error.IsEmpty())
|
if (!Error.IsEmpty())
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("error: %s: %s\n"), *WingUtils::FormatName(Pin), *Error);
|
WingOut::Stdout.Printf(TEXT("error: %s: %s\n"), *WingUtils::FormatName(Pin), *Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UWingServer::AddTouchedObject(Node);
|
UWingServer::AddTouchedObject(FoundNode);
|
||||||
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
|
K2Schema->TrySetDefaultValue(*Pin, Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Material graphs: set material expression properties.
|
// Material graphs: set material expression properties.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj)
|
void HandleMaterial(UEdGraph* GraphObj)
|
||||||
{
|
{
|
||||||
WingFetcher F(GraphObj, WingOut::Stdout);
|
WingFetcher F(GraphObj, WingOut::Stdout);
|
||||||
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
|
UEdGraphNode* FoundNode = F.Node(Node).Cast<UEdGraphNode>();
|
||||||
if (!Node) return;
|
if (!FoundNode) return;
|
||||||
|
|
||||||
TArray<FWingProperty> All = FWingProperty::GetDetails(Node, true);
|
TArray<FWingProperty> All = FWingProperty::GetDetails(FoundNode, true);
|
||||||
FWingProperty *P = WingUtils::FindOneWithExternalID(Entry.Name, All, TEXT("Property"), WingOut::Stdout);
|
FWingProperty *P = WingUtils::FindOneWithExternalID(Name, All, TEXT("Property"), WingOut::Stdout);
|
||||||
if (!P) return;
|
if (!P) return;
|
||||||
|
|
||||||
UWingServer::AddTouchedObject(Node);
|
UWingServer::AddTouchedObject(FoundNode);
|
||||||
|
|
||||||
if (!P->SetText(Entry.Value, WingOut::Stdout))
|
if (!P->SetText(Value, WingOut::Stdout))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,14 +107,8 @@ public:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FSetNodeDefaultEntry Entry;
|
if (K2Schema) HandleK2(GraphObj, K2Schema);
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
|
else if (MGSchema) HandleMaterial(GraphObj);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
WingOut::Stdout.Printf(TEXT("Done.\n"));
|
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 "WingServer.h"
|
||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
#include "WingFetcher.h"
|
#include "WingFetcher.h"
|
||||||
#include "WingProperty.h"
|
|
||||||
#include "WingUtils.h"
|
#include "WingUtils.h"
|
||||||
#include "EdGraph/EdGraph.h"
|
#include "EdGraph/EdGraph.h"
|
||||||
#include "EdGraph/EdGraphSchema.h"
|
#include "EdGraph/EdGraphSchema.h"
|
||||||
@@ -12,23 +11,6 @@
|
|||||||
#include "GraphPin_Connect.generated.h"
|
#include "GraphPin_Connect.generated.h"
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FConnectPinsEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString SourcePin;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString TargetPin;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class UWing_GraphPin_Connect : public UWingHandler
|
class UWing_GraphPin_Connect : public UWingHandler
|
||||||
{
|
{
|
||||||
@@ -38,8 +20,8 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of {sourcePin, targetPin} objects"))
|
UPROPERTY(EditAnywhere, meta=(Description="Alternating source pin / target pin strings"))
|
||||||
FWingJsonArray Connections;
|
FWingRestOfArgv SourcePin_TargetPin;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
@@ -54,24 +36,27 @@ public:
|
|||||||
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
|
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
|
||||||
if (!G) return;
|
if (!G) return;
|
||||||
|
|
||||||
int32 SuccessCount = 0;
|
if ((SourcePin_TargetPin.Argv.Num() % 2) != 0)
|
||||||
int32 TotalCount = Connections.Array.Num();
|
|
||||||
|
|
||||||
FConnectPinsEntry Entry;
|
|
||||||
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry);
|
|
||||||
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
|
|
||||||
{
|
{
|
||||||
if (!FWingProperty::PopulateFromJson(EntryProps, *ConnVal, false, WingOut::Stdout))
|
WingOut::Stdout.Print(TEXT("ERROR: SourcePin_TargetPin must contain an even number of arguments.\n"));
|
||||||
continue;
|
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);
|
WingFetcher FS(G, WingOut::Stdout);
|
||||||
UWingGraphPinRef* SourcePinRef = FS.Walk(Entry.SourcePin).Cast<UWingGraphPinRef>();
|
UWingGraphPinRef* SourcePinRef = FS.Walk(SourcePinPath).Cast<UWingGraphPinRef>();
|
||||||
if (!SourcePinRef) continue;
|
if (!SourcePinRef) continue;
|
||||||
UEdGraphPin* SourcePin = WingUtils::CheckGetPin(SourcePinRef->Node, SourcePinRef->PinName, WingOut::Stdout);
|
UEdGraphPin* SourcePin = WingUtils::CheckGetPin(SourcePinRef->Node, SourcePinRef->PinName, WingOut::Stdout);
|
||||||
if (!SourcePin) continue;
|
if (!SourcePin) continue;
|
||||||
|
|
||||||
WingFetcher FT(G, WingOut::Stdout);
|
WingFetcher FT(G, WingOut::Stdout);
|
||||||
UWingGraphPinRef* TargetPinRef = FT.Walk(Entry.TargetPin).Cast<UWingGraphPinRef>();
|
UWingGraphPinRef* TargetPinRef = FT.Walk(TargetPinPath).Cast<UWingGraphPinRef>();
|
||||||
if (!TargetPinRef) continue;
|
if (!TargetPinRef) continue;
|
||||||
UEdGraphPin* TargetPin = WingUtils::CheckGetPin(TargetPinRef->Node, TargetPinRef->PinName, WingOut::Stdout);
|
UEdGraphPin* TargetPin = WingUtils::CheckGetPin(TargetPinRef->Node, TargetPinRef->PinName, WingOut::Stdout);
|
||||||
if (!TargetPin) continue;
|
if (!TargetPin) continue;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include "WingServer.h"
|
#include "WingServer.h"
|
||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
#include "WingFetcher.h"
|
#include "WingFetcher.h"
|
||||||
#include "WingProperty.h"
|
|
||||||
#include "WingUtils.h"
|
#include "WingUtils.h"
|
||||||
#include "EdGraph/EdGraph.h"
|
#include "EdGraph/EdGraph.h"
|
||||||
#include "EdGraph/EdGraphPin.h"
|
#include "EdGraph/EdGraphPin.h"
|
||||||
@@ -24,8 +23,8 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of pin ID strings"))
|
UPROPERTY(EditAnywhere, meta=(Description="Pin ID strings"))
|
||||||
FWingJsonArray Pins;
|
FWingRestOfArgv Pins;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
@@ -43,15 +42,8 @@ public:
|
|||||||
int32 SuccessCount = 0;
|
int32 SuccessCount = 0;
|
||||||
int32 TotalDisconnected = 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);
|
WingFetcher FP(G, WingOut::Stdout);
|
||||||
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
|
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
|
||||||
if (!PinRef) continue;
|
if (!PinRef) continue;
|
||||||
@@ -72,6 +64,6 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
WingOut::Stdout.Printf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
|
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"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
|
||||||
FString 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;
|
bool Details = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "WingServer.h"
|
|
||||||
#include "WingBasics.h"
|
|
||||||
#include "Sequence.generated.h"
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UWing_Sequence : public UWingHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description=
|
|
||||||
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
|
|
||||||
FWingJsonArray Subcommands;
|
|
||||||
|
|
||||||
virtual void Register() override
|
|
||||||
{
|
|
||||||
UWingServer::AddHandler(this,
|
|
||||||
TEXT("Execute multiple commands in one request. Each subcommand "
|
|
||||||
"produces its own content block in the response. The big win "
|
|
||||||
"performance-wise is that fewer MCP calls means fewer "
|
|
||||||
"round-trip invocations of the LLM."));
|
|
||||||
}
|
|
||||||
virtual void Handle() override
|
|
||||||
{
|
|
||||||
// The actual code that implements Sequence is hardwired into
|
|
||||||
// WingServer. Because of that, this handler is never actually called
|
|
||||||
// under normal conditions. The handler exists for two reasons: to
|
|
||||||
// provide documentation, and also to catch the case where somebody
|
|
||||||
// nests a sequence inside another sequence.
|
|
||||||
WingOut::Stdout.Print(
|
|
||||||
TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
|
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
|
||||||
FString Query;
|
FString Query;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
|
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results"))
|
||||||
int32 Limit = 100;
|
int32 Limit = 100;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
|
|||||||
@@ -21,22 +21,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -46,10 +40,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, 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.Check(WingOut::Stdout)) return;
|
if (!Vars.Check(WingOut::Stdout)) return;
|
||||||
if (!Vars.Create(WingOut::Stdout)) return;
|
if (!Vars.Create(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
|
|||||||
@@ -21,23 +21,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Modify variables of a blueprint, function graph, "
|
TEXT("Add variables to a blueprint, function graph, "
|
||||||
"macro graph, event dispatcher graph, or custom event node. "));
|
"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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -47,10 +40,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, false, 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.Check(WingOut::Stdout)) return;
|
if (!Vars.Check(WingOut::Stdout)) return;
|
||||||
if (!Vars.Modify(WingOut::Stdout)) return;
|
if (!Vars.Modify(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
|
|||||||
@@ -21,22 +21,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
|
||||||
FString Object;
|
FString Object;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated"))
|
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
|
||||||
FString BlueprintVariables;
|
FWingRestOfArgv Variables;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
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
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -46,10 +39,7 @@ public:
|
|||||||
|
|
||||||
WingVariables Vars;
|
WingVariables Vars;
|
||||||
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
|
||||||
if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, WingOut::Stdout)) return;
|
if (!Vars.Parse(Variables.Argv, true, 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.Remove(WingOut::Stdout)) return;
|
if (!Vars.Remove(WingOut::Stdout)) return;
|
||||||
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
WingOut::Stdout.Printf(TEXT("Success.\n"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
|
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
|
||||||
FString Name;
|
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;
|
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;
|
bool IsVariable = false;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
@@ -117,6 +117,10 @@ public:
|
|||||||
BP->WidgetTree->RootWidget = NewWidget;
|
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);
|
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()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain *"))
|
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
|
||||||
FWingJsonArray Queries;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
|
|
||||||
int32 MaxResults = 50;
|
int32 MaxResults = 50;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, meta=(Description="Query strings; each may contain *"))
|
||||||
|
FWingRestOfArgv Queries;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
@@ -31,22 +31,8 @@ public:
|
|||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
// Validate all entries are strings before running any searches.
|
|
||||||
TArray<FString> QueryStrings;
|
|
||||||
QueryStrings.Reserve(Queries.Array.Num());
|
|
||||||
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
|
|
||||||
{
|
|
||||||
FString QueryStr;
|
|
||||||
if (!QueryVal->TryGetString(QueryStr))
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QueryStrings.Add(QueryStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
WingWidgets Widgets;
|
WingWidgets Widgets;
|
||||||
for (const FString& Query : QueryStrings)
|
for (const FString& Query : Queries.Argv)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||||
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
|
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "WingFetcher.h"
|
#include "WingFetcher.h"
|
||||||
#include "WingServer.h"
|
#include "WingServer.h"
|
||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
|
#include "WingProperty.h"
|
||||||
#include "WingUtils.h"
|
#include "WingUtils.h"
|
||||||
#include "WingComponent.h"
|
#include "WingComponent.h"
|
||||||
#include "Engine/Blueprint.h"
|
#include "Engine/Blueprint.h"
|
||||||
@@ -368,36 +369,27 @@ WingFetcher& WingFetcher::StructProp(const FString& Value)
|
|||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
FStructProperty* StructProp = nullptr;
|
TArray<FWingProperty> Details =
|
||||||
|
FWingProperty::GetDetails(Obj.Get(), true);
|
||||||
|
|
||||||
// The "host" is the object containing this property.
|
FWingProperty *WProp = WingUtils::FindOneWithInternalID(
|
||||||
UObject *HostObject = Obj.Get();
|
InternalID, Details, TEXT("Property"), WingOut::Stdout);
|
||||||
void *HostBase = Obj.Get();
|
if (!WProp) return SetError();
|
||||||
UStruct *HostType = Obj.Get()->GetClass();
|
|
||||||
bool HostEditable = true;
|
|
||||||
|
|
||||||
// If we are *already* inside a UWingStructRef, update the host
|
if (FStructProperty *FSProp = CastField<FStructProperty>(WProp->Prop))
|
||||||
// fields, to make it possible to navigate even further inside.
|
|
||||||
if (UWingStructRef *SPtr = ::Cast<UWingStructRef>(Obj.Get()))
|
|
||||||
{
|
{
|
||||||
HostObject = SPtr->Object;
|
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
||||||
HostBase = SPtr->StructBase;
|
Ptr->Object = WProp->Object.Get();
|
||||||
HostType = SPtr->StructType;
|
Ptr->StructType = FSProp->Struct;
|
||||||
HostEditable = SPtr->Editable;
|
Ptr->StructBase = FSProp->ContainerPtrToValuePtr<void>(WProp->Container);
|
||||||
|
Ptr->Editable = WProp->Editable;
|
||||||
|
SetObj(Ptr);
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
|
|
||||||
if (!StructProp)
|
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->GetName());
|
Errors.Printf(TEXT("Property %s is not a struct property.\n"),
|
||||||
|
*WProp->Prop->GetName());
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
|
||||||
Ptr->Object = HostObject;
|
|
||||||
Ptr->StructType = StructProp->Struct;
|
|
||||||
Ptr->StructBase = StructProp->ContainerPtrToValuePtr<void>(HostBase);
|
|
||||||
Ptr->Editable = HostEditable && StructProp->HasAllPropertyFlags(CPF_Edit);
|
|
||||||
SetObj(Ptr);
|
|
||||||
return *this;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,23 @@
|
|||||||
|
|
||||||
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT("ue-wingman "));
|
||||||
WingOut::Stdout.Print(Handler.Name);
|
WingOut::Stdout.Print(Handler.Name);
|
||||||
WingOut::Stdout.Print(TEXT("("));
|
|
||||||
bool bFirst = true;
|
|
||||||
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
|
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||||
{
|
{
|
||||||
if (!bFirst) WingOut::Stdout.Print(TEXT(","));
|
FStructProperty* StructProp = CastField<FStructProperty>(*PropIt);
|
||||||
bFirst = false;
|
const bool bIsRest =
|
||||||
if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?"));
|
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||||
WingOut::Stdout.Print(PropIt->GetName());
|
if (bIsRest)
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT(" [%s...]"), *PropIt->GetName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WingOut::Stdout.Print(TEXT(")\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
||||||
@@ -26,197 +32,206 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
|||||||
{
|
{
|
||||||
FProperty* Prop = *PropIt;
|
FProperty* Prop = *PropIt;
|
||||||
FString Name = Prop->GetName();
|
FString Name = Prop->GetName();
|
||||||
FString Type = UWingTypes::TypeToText(Prop);
|
FString Desc = Prop->GetMetaData(TEXT("Description"));
|
||||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
if (Desc.IsEmpty()) Desc = TEXT("No documentation");
|
||||||
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
|
|
||||||
|
|
||||||
if (bOptional)
|
WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc);
|
||||||
{
|
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
if (Handler.Documentation.IsEmpty()) return;
|
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)
|
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT("\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
PrintHandlerPrototype(Handler);
|
PrintHandlerPrototype(Handler);
|
||||||
PrintHandlerArguments(Handler);
|
PrintHandlerArguments(Handler);
|
||||||
PrintHandlerDescription(Handler);
|
PrintHandlerDescription(Handler);
|
||||||
WingOut::Stdout.Print(TEXT("\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::FetcherPaths()
|
void UWingManualSections::FetcherPaths()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n FETCHER PATHS:"
|
"\n FETCHER PATHS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Most commands require you to specify a 'fetcher path'."
|
"\n Most commands require you to specify a 'fetcher path'."
|
||||||
"\n A fetcher path starts with an asset name, followed by"
|
"\n A fetcher path starts with an asset name, followed by"
|
||||||
"\n steps that navigate into the asset. Some Examples:"
|
"\n steps that navigate into the asset. Some Examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
|
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
|
||||||
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
|
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
|
||||||
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
|
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
|
||||||
"\n"
|
"\n"
|
||||||
"\n The navigation steps supported are:"
|
"\n The navigation steps supported are:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n graph — move from a blueprint or material to a graph."
|
"\n graph — move from a blueprint or material to a graph."
|
||||||
"\n node — move from a graph to a graph node"
|
"\n node — move from a graph to a graph node"
|
||||||
"\n pin — move from a graph node to a pin"
|
"\n pin — move from a graph node to a pin"
|
||||||
"\n component — move from a blueprint to a component"
|
"\n component — move from a blueprint to a component"
|
||||||
"\n levelblueprint — move from a world to a blueprint"
|
"\n levelblueprint — move from a world to a blueprint"
|
||||||
"\n widget — move from a widget blueprint to a widget"
|
"\n widget — move from a widget blueprint to a widget"
|
||||||
"\n structprop — move into a struct property of an object"
|
"\n structprop — move into a struct property of an object"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Notice that paths use escaped fnames. See the section"
|
"\n Notice that paths use escaped fnames. See the section"
|
||||||
"\n on escape sequences in fnames below sfor more information."
|
"\n on escape sequences in fnames below sfor more information."
|
||||||
"\n"
|
"\n"
|
||||||
"\n Steps do not always require a parameter. For example, materials"
|
"\n Steps do not always require a parameter. For example, materials"
|
||||||
"\n only have one graph, so you can just say:"
|
"\n only have one graph, so you can just say:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n /Game/Materials/MyMaterial,graph"
|
"\n /Game/Materials/MyMaterial,graph"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::ExpressingTypes()
|
void UWingManualSections::ExpressingTypes()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n EXPRESSING TYPES:"
|
"\n EXPRESSING TYPES:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n To change the type of a variable, or to express function parameters,"
|
"\n To change the type of a variable, or to express function parameters,"
|
||||||
"\n you will use our syntax for types. Here are some valid examples:"
|
"\n you will use our syntax for types. Here are some valid examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
|
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
|
||||||
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
|
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
|
||||||
"\n Array<Int>, Set<String>, Map<Int,Actor>"
|
"\n Array<Int>, Set<String>, Map<Int,Actor>"
|
||||||
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
|
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
|
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
|
||||||
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
|
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
|
||||||
"\n is used as a type, the typename is just BP_Foo. You can search"
|
"\n is used as a type, the typename is just BP_Foo. You can search"
|
||||||
"\n for valid types using the TypeName_Search command."
|
"\n for valid types using the TypeName_Search command."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::VariableDeclarations()
|
void UWingManualSections::VariableDeclarations()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n VARIABLE DECLARATIONS:"
|
"\n VARIABLE DECLARATIONS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n We have our own syntax for variable declarations: a type,"
|
"\n We have our own syntax for variable declarations:"
|
||||||
"\n a name, optional flags, and an optional default value,"
|
"\n"
|
||||||
"\n always on one line:"
|
"\n kind type name (optional flags) = optional default value"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Array<Actor> Actors"
|
"\n Kind can be:"
|
||||||
"\n Float F (InstanceEditable)"
|
"\n"
|
||||||
"\n String S = This is the default value"
|
"\n blueprint - eg, instance variables"
|
||||||
"\n"
|
"\n input - a function argument or macro input"
|
||||||
"\n The commands Variables_Add, Variables_Modify,"
|
"\n output - a function return value or macro output"
|
||||||
"\n and Variables_Remove can be used to edit "
|
"\n local - local variables"
|
||||||
"\n blueprint variables, graph local variables, graph input"
|
"\n"
|
||||||
"\n variables, graph output variables, and custom"
|
"\n Here are some examples:"
|
||||||
"\n event node input variables. Event dispatchers are"
|
"\n"
|
||||||
"\n also graphs, so they too can be edited."
|
"\n input Array<Actor> Actors"
|
||||||
"\n"
|
"\n output Float F (InstanceEditable)"
|
||||||
));
|
"\n blueprint String S = This is the default value"
|
||||||
|
"\n"
|
||||||
|
"\n The commands Variables_Add, Variables_Modify,"
|
||||||
|
"\n and Variables_Remove can be used to edit "
|
||||||
|
"\n blueprint variables, graph local variables, graph input"
|
||||||
|
"\n variables, graph output variables, and custom"
|
||||||
|
"\n event node input variables. Event dispatchers are"
|
||||||
|
"\n also graphs, so they too can be edited."
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::EscapeSequencesInFNames()
|
void UWingManualSections::EscapeSequencesInFNames()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n ESCAPE SEQUENCES IN FNAMES:"
|
"\n ESCAPE SEQUENCES IN FNAMES:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n When we output FNames, we use HTML escape sequences for the"
|
"\n When we output FNames, we use HTML escape sequences for the"
|
||||||
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
|
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
|
||||||
"\n We also translate spaces to periods."
|
"\n We also translate spaces to periods."
|
||||||
"\n"
|
"\n"
|
||||||
"\n When sending FNames to UE Wingman, you *must* escape the marks"
|
"\n When sending FNames to UE Wingman, you *must* escape the marks"
|
||||||
"\n listed above, but you *may* escape any character. To send an FName"
|
"\n listed above, but you *may* escape any character. To send an FName"
|
||||||
"\n with a space in it, either use   or a period."
|
"\n with a space in it, either use   or a period."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::MaterialEditing()
|
void UWingManualSections::MaterialEditing()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n MATERIAL EDITING:"
|
"\n MATERIAL EDITING:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n We do not expose material expressions directly. Instead, you"
|
"\n We do not expose material expressions directly. Instead, you"
|
||||||
"\n will be editing the material graph. However, if you Graph_Dump"
|
"\n will be editing the material graph. However, if you Graph_Dump"
|
||||||
"\n a material graph, you will see that the nodes contain"
|
"\n a material graph, you will see that the nodes contain"
|
||||||
"\n properties which actually come from the material expressions."
|
"\n properties which actually come from the material expressions."
|
||||||
"\n You can edit these using Details_Set on the node."
|
"\n You can edit these using Details_Set on the node."
|
||||||
"\n"
|
"\n"
|
||||||
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
|
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
|
||||||
"\n a single node what would otherwise take many.\n"
|
"\n a single node what would otherwise take many.\n"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::NodeContextMenus()
|
void UWingManualSections::NodeContextMenus()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n NODE CONTEXT MENUS:"
|
"\n NODE CONTEXT MENUS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
|
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
|
||||||
"\n to the node context menu. This menu includes both node"
|
"\n to the node context menu. This menu includes both node"
|
||||||
"\n operations and pin operations (e.g. Split Struct Pin,"
|
"\n operations and pin operations (e.g. Split Struct Pin,"
|
||||||
"\n Add Pin)."
|
"\n Add Pin)."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::VariableGettersAndSetters()
|
void UWingManualSections::VariableGettersAndSetters()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n VARIABLE GETTERS AND SETTERS:"
|
"\n VARIABLE GETTERS AND SETTERS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Access to local vars, function parameters, and "
|
"\n Access to local vars, function parameters, and "
|
||||||
"\n blueprint vars is through getter and setter nodes. "
|
"\n blueprint vars is through getter and setter nodes. "
|
||||||
"\n These can be found in GraphNode_SearchTypes by "
|
"\n These can be found in GraphNode_SearchTypes by "
|
||||||
"\n searching for 'Variable'. Some examples:"
|
"\n searching for 'Variable'. Some examples:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
|
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
|
||||||
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
|
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
|
||||||
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
|
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWingManualSections::BestPerformance()
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT(
|
||||||
|
"\n BEST PERFORMANCE:"
|
||||||
|
"\n"
|
||||||
|
"\n UE Wingman is much faster than the LLM. Therefore, it is"
|
||||||
|
"\n advantageous to batch: chain multiple ue-wingman commands"
|
||||||
|
"\n together using bash semicolon."
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingManualSections::ImportantCommands()
|
void UWingManualSections::ImportantCommands()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n IMPORTANT COMMANDS:"
|
"\n IMPORTANT COMMANDS:"
|
||||||
"\n"
|
"\n"
|
||||||
"\n Documentation_Manual: print manual sections"
|
"\n Documentation_Manual: print the entire manual"
|
||||||
"\n Documentation_Commands: a list of all the main commands"
|
"\n Documentation_Section: print a single section of the manual"
|
||||||
"\n Documentation_CreateAssets: Additional commands that create new assets"
|
"\n Documentation_Commands: print concise list of all ue-wingman commands"
|
||||||
"\n Blueprint_Dump: a summary of any blueprint"
|
"\n Documentation_Command: detailed documentation for a single ue-wingman command"
|
||||||
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
"\n Documentation_CreateAssets: list of commands that create new assets"
|
||||||
"\n Details_Dump: Dump the details panel for a given object"
|
"\n Blueprint_Dump: a summary of any blueprint"
|
||||||
"\n Details_Set: Manipulate the details panel for a given object"
|
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
||||||
"\n Sequence: Batch commands together for faster execution"
|
"\n Details_Dump: Dump the details panel for a given object"
|
||||||
"\n"
|
"\n Details_Set: Manipulate the details panel for a given object"
|
||||||
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
|
"\n"
|
||||||
"\n to get detailed help for a specific command."
|
));
|
||||||
"\n"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TSet<FName> WingManual::GetSections()
|
TSet<FName> WingManual::GetSections()
|
||||||
@@ -232,7 +247,7 @@ TSet<FName> WingManual::GetSections()
|
|||||||
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
|
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
|
||||||
{
|
{
|
||||||
if (Sections.IsEmpty()) return;
|
if (Sections.IsEmpty()) return;
|
||||||
if (Prefix) Output.Print(Prefix);
|
if (Prefix) Output.Print(Prefix);
|
||||||
bool bFirst = true;
|
bool bFirst = true;
|
||||||
for (const FName& Section : Sections)
|
for (const FName& Section : Sections)
|
||||||
{
|
{
|
||||||
@@ -240,7 +255,7 @@ void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Secti
|
|||||||
bFirst = false;
|
bFirst = false;
|
||||||
Output.Printf(TEXT("%s"), *Section.ToString());
|
Output.Printf(TEXT("%s"), *Section.ToString());
|
||||||
}
|
}
|
||||||
if (Prefix) Output.Print(TEXT("\n"));
|
if (Prefix) Output.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WingManual::PrintSection(FName Section)
|
bool WingManual::PrintSection(FName Section)
|
||||||
@@ -256,10 +271,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
|
|||||||
FString QueryLower = Query.ToLower();
|
FString QueryLower = Query.ToLower();
|
||||||
FString PrevGroup;
|
FString PrevGroup;
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
|
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
|
||||||
{
|
{
|
||||||
if (H.Kind != Kind) continue;
|
if (H.Kind != Kind) continue;
|
||||||
if (!H.Name.ToLower().Contains(QueryLower)) continue;
|
if (!H.Name.ToLower().Contains(QueryLower)) continue;
|
||||||
|
any = true;
|
||||||
|
|
||||||
// Blank line between groups
|
// Blank line between groups
|
||||||
if (!Verbose)
|
if (!Verbose)
|
||||||
@@ -278,4 +295,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
|
|||||||
else
|
else
|
||||||
PrintHandlerPrototype(H);
|
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.
|
// Parse the value string.
|
||||||
FWingParameterEditor Editor;
|
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);
|
Meta->Value = GS->Getter(Editor);
|
||||||
|
|
||||||
// Apply the update.
|
// Apply the update.
|
||||||
@@ -308,7 +308,7 @@ void FWingParameterEditor::Print(const Info &ID, const Metadata &Meta)
|
|||||||
}
|
}
|
||||||
FWingParameterEditor Editor;
|
FWingParameterEditor Editor;
|
||||||
GS->Setter(Editor, Meta.Value);
|
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"),
|
WingOut::Stdout.Printf(TEXT(" %s %s\n"),
|
||||||
*StringID(ID), *StrVal);
|
*StringID(ID), *StrVal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
|
|||||||
return true;
|
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
|
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
|
||||||
{
|
{
|
||||||
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
|
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
|
||||||
@@ -378,25 +327,25 @@ FString FWingProperty::GetCategory() const
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FWingProperty> FWingProperty::GetAll(FWingStructAndUStruct Obj)
|
TArray<FWingProperty> FWingProperty::GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
|
||||||
{
|
{
|
||||||
TArray<FWingProperty> Result;
|
TArray<FWingProperty> Result;
|
||||||
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
|
for (TFieldIterator<FProperty> It(Struct); It; ++It)
|
||||||
{
|
{
|
||||||
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
|
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||||
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
|
Result.Emplace(Obj, Container, *It, Editable);
|
||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FWingProperty> FWingProperty::GetVisible(FWingStructAndUStruct Obj)
|
TArray<FWingProperty> FWingProperty::GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
|
||||||
{
|
{
|
||||||
TArray<FWingProperty> Result;
|
TArray<FWingProperty> Result;
|
||||||
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
|
for (TFieldIterator<FProperty> It(Struct); It; ++It)
|
||||||
{
|
{
|
||||||
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
|
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
|
||||||
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
|
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
|
||||||
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
|
Result.Emplace(Obj, Container, *It, Editable);
|
||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -436,10 +385,8 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
// of the struct instead. Propagate editability of the host.
|
// of the struct instead. Propagate editability of the host.
|
||||||
if (UWingStructRef *SP = Cast<UWingStructRef>(Obj))
|
if (UWingStructRef *SP = Cast<UWingStructRef>(Obj))
|
||||||
{
|
{
|
||||||
TArray<FWingProperty> Result =
|
return GetVisible(SP->Object, SP->StructBase,
|
||||||
GetVisible(FWingStructAndUStruct(SP->StructBase, SP->StructType));
|
SP->StructType, Mutable && SP->Editable);
|
||||||
if (!Mutable || (!SP->Editable)) StripEditable(Result);
|
|
||||||
return Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blueprints don't have editable properties. So
|
// Blueprints don't have editable properties. So
|
||||||
@@ -465,7 +412,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FWingProperty> Result = GetVisible(Obj);
|
TArray<FWingProperty> Result = GetVisible(Obj, Mutable);
|
||||||
|
|
||||||
// If it's a Material Graph node, also collect properties from
|
// If it's a Material Graph node, also collect properties from
|
||||||
// the associated material expression.
|
// the associated material expression.
|
||||||
@@ -474,7 +421,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
{
|
{
|
||||||
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
|
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
|
||||||
{
|
{
|
||||||
Result.Append(GetVisible(Expr));
|
Result.Append(GetVisible(Expr, Mutable));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,66 +433,63 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
FWingProperty::Remove(Result, TEXT("Slot"));
|
FWingProperty::Remove(Result, TEXT("Slot"));
|
||||||
if (UPanelSlot* Slot = Widget->Slot)
|
if (UPanelSlot* Slot = Widget->Slot)
|
||||||
{
|
{
|
||||||
Result.Append(GetVisible(Slot));
|
Result.Append(GetVisible(Slot, Mutable));
|
||||||
}
|
}
|
||||||
FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable"));
|
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;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
|
||||||
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors)
|
|
||||||
{
|
{
|
||||||
bool Ok = true;
|
int32 ArgIndex = 0;
|
||||||
|
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
|
FWingProperty& P = Props[PropIndex];
|
||||||
if (!KnownKeys.Contains(Name))
|
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
|
||||||
{
|
const bool bIsRest =
|
||||||
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||||
Ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate each property from JSON
|
if (bIsRest)
|
||||||
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 (PropIndex + 1 != Props.Num())
|
||||||
if (!Optional)
|
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
|
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
|
||||||
Ok = false;
|
*WingUtils::FormatName(P.Prop));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FWingRestOfArgv Rest;
|
||||||
|
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
|
||||||
|
{
|
||||||
|
Rest.Argv.Add(Argv[I]);
|
||||||
|
}
|
||||||
|
P.Prop->SetValue_InContainer(P.Container, &Rest);
|
||||||
|
ArgIndex = Argv.Num();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!P.SetJson(*Value, Errors)) Ok = false;
|
|
||||||
}
|
|
||||||
return Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
|
if (ArgIndex >= Argv.Num())
|
||||||
{
|
{
|
||||||
// Make sure they passed in a JSON object.
|
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
|
||||||
TSharedPtr<FJsonObject> Obj = Json.AsObject();
|
*WingUtils::FormatName(P.Prop));
|
||||||
if (Obj == nullptr)
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
|
||||||
|
ArgIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArgIndex < Argv.Num())
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("property data should be stored in a json object\n"));
|
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
|
||||||
|
*Argv[ArgIndex]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -580,11 +524,6 @@ bool FWingProperty::CheckImportTextResult(const FString &Value, WingOut Errors)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FWingProperty::StripEditable(TArray<FWingProperty> &Props)
|
|
||||||
{
|
|
||||||
for (FWingProperty &Elt : Props) Elt.Editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FWingProperty::CheckEditable(WingOut Errors) const
|
bool FWingProperty::CheckEditable(WingOut Errors) const
|
||||||
{
|
{
|
||||||
if (!Editable)
|
if (!Editable)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "UObject/StrongObjectPtr.h"
|
#include "UObject/StrongObjectPtr.h"
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "Misc/CoreDelegates.h"
|
||||||
#include "Misc/OutputDeviceRedirector.h"
|
#include "Misc/OutputDeviceRedirector.h"
|
||||||
#include "Serialization/JsonReader.h"
|
#include "Serialization/JsonReader.h"
|
||||||
#include "Serialization/JsonSerializer.h"
|
#include "Serialization/JsonSerializer.h"
|
||||||
@@ -55,8 +56,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildWingHandlerRegistry();
|
LoadingPhasesCompleteHandle = FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UWingServer::BuildWingHandlerRegistry);
|
||||||
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddUObject(this, &UWingServer::OnModulesChanged);
|
|
||||||
LogCapture.bEnabled = false;
|
LogCapture.bEnabled = false;
|
||||||
GLog->AddOutputDevice(&LogCapture);
|
GLog->AddOutputDevice(&LogCapture);
|
||||||
bRunning = true;
|
bRunning = true;
|
||||||
@@ -65,7 +65,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
|
|||||||
|
|
||||||
void UWingServer::Deinitialize()
|
void UWingServer::Deinitialize()
|
||||||
{
|
{
|
||||||
FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle);
|
FCoreDelegates::OnAllModuleLoadingPhasesComplete.Remove(LoadingPhasesCompleteHandle);
|
||||||
|
|
||||||
if (!bRunning)
|
if (!bRunning)
|
||||||
{
|
{
|
||||||
@@ -81,7 +81,7 @@ void UWingServer::Deinitialize()
|
|||||||
bShuttingDown = true;
|
bShuttingDown = true;
|
||||||
for (auto& Msg : PendingMessages)
|
for (auto& Msg : PendingMessages)
|
||||||
{
|
{
|
||||||
Msg->Response.SetValue(FString());
|
Msg->Response.SetValue(TArray<uint8>());
|
||||||
}
|
}
|
||||||
PendingMessages.Empty();
|
PendingMessages.Empty();
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime)
|
|||||||
// If we have a request, process it.
|
// If we have a request, process it.
|
||||||
if (Request.IsValid())
|
if (Request.IsValid())
|
||||||
{
|
{
|
||||||
FString Response = HandleRequest(Request->Line);
|
TArray<uint8> Response = HandleRequest(Request->Request);
|
||||||
Request->Response.SetValue(Response);
|
Request->Response.SetValue(Response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,71 +169,24 @@ TStatId UWingServer::GetStatId() const
|
|||||||
// HandleRequest — Given a command, execute it.
|
// HandleRequest — Given a command, execute it.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
FString UWingServer::HandleRequest(const FString& Line)
|
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
|
||||||
{
|
{
|
||||||
// Parse the request as JSON before doing anything else.
|
TArray<FString> Argv;
|
||||||
TSharedPtr<FJsonValue> Value;
|
FString ResponseText;
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
|
||||||
if (!FJsonSerializer::Deserialize(Reader, Value))
|
|
||||||
return PackageResponses({TEXT("Invalid Json")});
|
|
||||||
|
|
||||||
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
|
|
||||||
if (!Value->TryGetObject(RequestPtr))
|
|
||||||
return PackageResponses({TEXT("Json must be an object")});
|
|
||||||
TSharedPtr<FJsonObject> Request = *RequestPtr;
|
|
||||||
|
|
||||||
FString Command;
|
if (DeserializeArgv(RequestBytes, Argv))
|
||||||
Request->TryGetStringField(TEXT("command"), Command);
|
|
||||||
if (Command == TEXT("Sequence"))
|
|
||||||
{
|
{
|
||||||
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
|
PreCallHandler();
|
||||||
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
|
TryCallHandler(Argv);
|
||||||
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
|
ResponseText = PostCallHandler();
|
||||||
|
|
||||||
TArray<FString> Responses;
|
|
||||||
Responses.Reserve(Subcommands->Num());
|
|
||||||
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
|
|
||||||
{
|
|
||||||
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
|
|
||||||
if (!Sub->TryGetObject(SubObjPtr))
|
|
||||||
Responses.Add(TEXT("Subcommand must be a JSON object."));
|
|
||||||
else
|
|
||||||
Responses.Add(HandleJsonRequest(*SubObjPtr));
|
|
||||||
}
|
|
||||||
return PackageResponses(Responses);
|
|
||||||
}
|
}
|
||||||
|
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
|
||||||
|
|
||||||
return PackageResponses({HandleJsonRequest(Request)});
|
FTCHARToUTF8 Utf8(*ResponseText);
|
||||||
|
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
|
void UWingServer::PreCallHandler()
|
||||||
{
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Blocks;
|
|
||||||
Blocks.Reserve(Responses.Num());
|
|
||||||
for (const FString& Response : Responses)
|
|
||||||
{
|
|
||||||
// Unreal's JSON writer terminates string serialization at the first
|
|
||||||
// embedded null byte rather than escaping it, which would silently
|
|
||||||
// truncate output. Sanitize null bytes to spaces.
|
|
||||||
FString Sanitized = Response;
|
|
||||||
for (int32 i = 0; i < Sanitized.Len(); ++i)
|
|
||||||
{
|
|
||||||
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
|
|
||||||
Block->SetStringField(TEXT("type"), TEXT("text"));
|
|
||||||
Block->SetStringField(TEXT("text"), Sanitized);
|
|
||||||
Blocks.Add(MakeShared<FJsonValueObject>(Block));
|
|
||||||
}
|
|
||||||
|
|
||||||
FString OutJson;
|
|
||||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
|
|
||||||
FJsonSerializer::Serialize(Blocks, Writer);
|
|
||||||
return OutJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|
||||||
{
|
{
|
||||||
LogCapture.CapturedErrors.Empty();
|
LogCapture.CapturedErrors.Empty();
|
||||||
LogCapture.bEnabled = true;
|
LogCapture.bEnabled = true;
|
||||||
@@ -241,9 +194,10 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|||||||
SuggestedManualSections.Empty();
|
SuggestedManualSections.Empty();
|
||||||
bSuggestHandlerHelp = false;
|
bSuggestHandlerHelp = false;
|
||||||
LastHandler = nullptr;
|
LastHandler = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
TryCallHandler(Request);
|
FString UWingServer::PostCallHandler()
|
||||||
|
{
|
||||||
Notifier.SendNotifications();
|
Notifier.SendNotifications();
|
||||||
LogCapture.bEnabled = false;
|
LogCapture.bEnabled = false;
|
||||||
for (const FString& Msg : LogCapture.CapturedErrors)
|
for (const FString& Msg : LogCapture.CapturedErrors)
|
||||||
@@ -269,17 +223,21 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
|
||||||
{
|
{
|
||||||
// Extract the command from the request.
|
FString Command = "Documentation_Manual";
|
||||||
FString Command;
|
if (Argv.Num() > 0)
|
||||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
|
Command = Argv[0];
|
||||||
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
|
Argv = Argv.RightChop(1);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
Request->RemoveField(TEXT("command"));
|
|
||||||
|
if ((Command.Equals(TEXT("--help"))) ||
|
||||||
|
(Command.Equals(TEXT("-help"))) ||
|
||||||
|
(Command.Equals(TEXT("help"))))
|
||||||
|
{
|
||||||
|
Command = "Documentation_Manual";
|
||||||
|
}
|
||||||
|
|
||||||
// Find the handler for the specified command.
|
// Find the handler for the specified command.
|
||||||
FWingHandlerConfig* Found = FindHandler(Command);
|
FWingHandlerConfig* Found = FindHandler(Command);
|
||||||
@@ -296,9 +254,9 @@ void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
|
|||||||
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
|
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
|
||||||
Handler->Configuration = Found;
|
Handler->Configuration = Found;
|
||||||
|
|
||||||
// Populate the handler object with the request parameters.
|
// Populate the handler object with argv parameters.
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler);
|
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
||||||
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
|
if (!FWingProperty::PopulateFromArgv(Props, Argv, WingOut::Stdout))
|
||||||
{
|
{
|
||||||
UWingServer::SuggestHandlerHelp();
|
UWingServer::SuggestHandlerHelp();
|
||||||
return;
|
return;
|
||||||
@@ -356,102 +314,105 @@ void UWingServer::CleanupFinishedClients()
|
|||||||
|
|
||||||
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
|
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
|
||||||
{
|
{
|
||||||
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
|
||||||
constexpr int32 MinUnusedRecvSpace = 4096;
|
|
||||||
|
|
||||||
FSocket* Socket = Client->Socket;
|
FSocket* Socket = Client->Socket;
|
||||||
TArray<uint8> RecvBuf;
|
|
||||||
RecvBuf.SetNumUninitialized(MinUnusedRecvSpace);
|
|
||||||
int32 RecvLen = 0;
|
|
||||||
|
|
||||||
WaitForAssetRegistry();
|
WaitForAssetRegistry();
|
||||||
|
|
||||||
while (true)
|
TArray<uint8> Request;
|
||||||
|
if (!ReceiveRequest(Socket, Request))
|
||||||
{
|
{
|
||||||
FString Request;
|
Client->bDone = true;
|
||||||
if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request))
|
return;
|
||||||
{
|
|
||||||
FString Response;
|
|
||||||
if (!ProcessRequestOnGameThread(Request, Response))
|
|
||||||
{
|
|
||||||
Client->bDone = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the response back, null-terminated (blocking)
|
|
||||||
FTCHARToUTF8 Utf8(*Response);
|
|
||||||
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
|
|
||||||
Utf8.Length() + 1))
|
|
||||||
{
|
|
||||||
Client->bDone = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TArray<uint8> Response;
|
||||||
|
if (!ProcessRequestOnGameThread(Request, Response))
|
||||||
|
{
|
||||||
|
Client->bDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendAll(Socket, Response.GetData(), Response.Num());
|
||||||
Client->bDone = true;
|
Client->bDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ExtractRequestFromBuffer(
|
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
|
||||||
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest)
|
|
||||||
{
|
{
|
||||||
const uint8* EndOfRequest = static_cast<const uint8*>(
|
return
|
||||||
memchr(RecvBuf.GetData(), '\0', RecvLen));
|
((uint32)Data[0] << 24) |
|
||||||
if (EndOfRequest == nullptr)
|
((uint32)Data[1] << 16) |
|
||||||
|
((uint32)Data[2] << 8) |
|
||||||
|
(uint32)Data[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UWingServer::DeserializeArgv(
|
||||||
|
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
|
||||||
|
int32 Offset = 0;
|
||||||
|
while (Offset < RequestBytes.Num())
|
||||||
{
|
{
|
||||||
return false;
|
if (RequestBytes.Num() - Offset < 4)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
|
||||||
|
Offset += 4;
|
||||||
|
|
||||||
|
if ((uint32)(RequestBytes.Num() - Offset) < Length)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Argv.Add(FString::ConstructFromPtrSize(
|
||||||
|
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
|
||||||
|
Length));
|
||||||
|
Offset += (int32)Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 MessageLen =
|
|
||||||
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
|
|
||||||
OutRequest = FString::ConstructFromPtrSize(
|
|
||||||
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
|
|
||||||
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
|
|
||||||
if (RemainingBytes > 0)
|
|
||||||
{
|
|
||||||
FMemory::Memmove(
|
|
||||||
RecvBuf.GetData(),
|
|
||||||
RecvBuf.GetData() + MessageLen + 1,
|
|
||||||
RemainingBytes);
|
|
||||||
}
|
|
||||||
RecvLen = RemainingBytes;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ReceiveMoreBytesIntoBuffer(
|
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
|
||||||
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen)
|
|
||||||
{
|
{
|
||||||
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
||||||
constexpr int32 MinUnusedRecvSpace = 4096;
|
constexpr int32 ChunkSize = 8192;
|
||||||
|
|
||||||
int32 UnusedSpace = RecvBuf.Num() - RecvLen;
|
TArray<uint8> RecvBuf;
|
||||||
if (UnusedSpace < MinUnusedRecvSpace)
|
RecvBuf.Reserve(ChunkSize);
|
||||||
|
|
||||||
|
// Unreal's FSocket API is fundamentally broken: recv cannot
|
||||||
|
// differentiate between a socket that has been cleanly closed
|
||||||
|
// and a socket that has had an error. So we have no choice
|
||||||
|
// but to just read until recv returns false (which could be a
|
||||||
|
// clean close or an error). Then, we check if we have a cleanly
|
||||||
|
// encoded payload: if so, we assume everything is fine.
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
if (RecvBuf.Num() >= MaxRecvBufBytes)
|
uint8 Temp[ChunkSize];
|
||||||
|
int32 BytesRead = 0;
|
||||||
|
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (BytesRead <= 0) break;
|
||||||
|
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
|
RecvBuf.Append(Temp, BytesRead);
|
||||||
UnusedSpace = RecvBuf.Num() - RecvLen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 BytesRead = 0;
|
if (RecvBuf.Num() < 4) return false;
|
||||||
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
|
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
|
||||||
{
|
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
|
||||||
return false;
|
RecvBuf.RemoveAt(0, 4);
|
||||||
}
|
|
||||||
if (BytesRead <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecvLen += BytesRead;
|
OutRequest = MoveTemp(RecvBuf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,13 +432,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool UWingServer::ProcessRequestOnGameThread(
|
bool UWingServer::ProcessRequestOnGameThread(
|
||||||
const FString& Request, FString& Response)
|
const TArray<uint8>& Request, TArray<uint8>& Response)
|
||||||
{
|
{
|
||||||
// Enqueue the message for game-thread processing.
|
// Enqueue the message for game-thread processing.
|
||||||
TSharedPtr<UWingServer::FPendingMessage> Msg =
|
TSharedPtr<UWingServer::FPendingMessage> Msg =
|
||||||
MakeShared<UWingServer::FPendingMessage>();
|
MakeShared<UWingServer::FPendingMessage>();
|
||||||
Msg->Line = Request;
|
Msg->Request = Request;
|
||||||
TFuture<FString> Future = Msg->Response.GetFuture();
|
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
|
||||||
|
|
||||||
{
|
{
|
||||||
FScopeLock Lock(&GWingServer->Mutex);
|
FScopeLock Lock(&GWingServer->Mutex);
|
||||||
@@ -534,11 +495,6 @@ void UWingServer::BuildWingHandlerRegistry()
|
|||||||
WingHandlerRegistry.Sort([](const FWingHandlerConfig& A, const FWingHandlerConfig& B) { return A.Name < B.Name; });
|
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)
|
FWingHandlerConfig* UWingServer::FindHandler(const FString& Name)
|
||||||
{
|
{
|
||||||
int32 Index = Algo::LowerBoundBy(WingHandlerRegistry, Name, [](const FWingHandlerConfig& H) { return H.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()))
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
for (const Var &Variable : Variables)
|
for (const Var &Variable : Variables)
|
||||||
@@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
|
|||||||
return true;
|
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()
|
void WingVariables::Empty()
|
||||||
{
|
{
|
||||||
BlueprintVariables.Empty();
|
BlueprintVariables.Empty();
|
||||||
@@ -254,6 +144,107 @@ void WingVariables::Print(WingOut Out)
|
|||||||
OutputVariables.Print(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)
|
void WingVariables::Load(WingOut Errors)
|
||||||
{
|
{
|
||||||
Empty();
|
Empty();
|
||||||
@@ -325,7 +316,7 @@ WingVariables::Var WingVariables::LoadBlueprintVariableDescription(FBPVariableDe
|
|||||||
FProperty* Prop = CDO->GetClass()->FindPropertyByName(Desc.VarName);
|
FProperty* Prop = CDO->GetClass()->FindPropertyByName(Desc.VarName);
|
||||||
if (Prop)
|
if (Prop)
|
||||||
{
|
{
|
||||||
Result.DefaultValue = FWingProperty(Prop, CDO, false).GetText();
|
Result.DefaultValue = FWingProperty(CDO, CDO, Prop, false).GetText();
|
||||||
Result.DefaultSpecified = true;
|
Result.DefaultSpecified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +491,7 @@ bool WingVariables::ModifyBlueprintDefaults(WingOut Errors)
|
|||||||
*WingTokenizer::ExternalizeID(Input.Name));
|
*WingTokenizer::ExternalizeID(Input.Name));
|
||||||
return false;
|
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;
|
return true;
|
||||||
|
|||||||
@@ -62,32 +62,18 @@ public:
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Json wrappers.
|
// A simple type to store the remaining arguments in
|
||||||
//
|
// an Argv Array.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
USTRUCT()
|
USTRUCT()
|
||||||
struct FWingJsonObject
|
struct FWingRestOfArgv
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
TSharedPtr<FJsonObject> Json;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Marker struct for handler parameters that accept a JSON array.
|
UPROPERTY()
|
||||||
// PopulateFromJson stashes the actual JSON array into the Array field.
|
TArray<FString> Argv;
|
||||||
//
|
|
||||||
USTRUCT()
|
|
||||||
struct FWingJsonArray
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Array;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -129,38 +115,6 @@ private:
|
|||||||
FStringBuilderBase *Buffer;
|
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.
|
// References.
|
||||||
@@ -224,11 +178,12 @@ class UWingStructRef : public UObject
|
|||||||
public:
|
public:
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UObject* Object;
|
UObject* Object;
|
||||||
|
|
||||||
|
void *StructBase;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UStruct* StructType;
|
UStruct* StructType;
|
||||||
|
|
||||||
void *StructBase;
|
|
||||||
|
|
||||||
bool Editable;
|
bool Editable;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,7 @@ public:
|
|||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
static void VariableGettersAndSetters();
|
static void VariableGettersAndSetters();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void BestPerformance();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,20 +3,24 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "WingBasics.h"
|
#include "WingBasics.h"
|
||||||
|
|
||||||
// A resolved property: the FProperty descriptor plus a pointer to
|
|
||||||
// the value's storage.
|
|
||||||
//
|
|
||||||
struct FWingProperty
|
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;
|
void* Container = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
bool Editable = false;
|
bool Editable = false;
|
||||||
|
|
||||||
// Construct a property reference.
|
// Construct a property reference.
|
||||||
//
|
//
|
||||||
FWingProperty(FProperty* InProp, void* InContainer, bool Edit)
|
FWingProperty(UObject *InObject, void* InContainer, FProperty* InProp, bool Edit)
|
||||||
: Prop(InProp), Container(InContainer), Editable(Edit) {}
|
: Object(InObject), Container(InContainer), Prop(InProp), Editable(Edit) {}
|
||||||
|
|
||||||
// Construct a null property reference.
|
// Construct a null property reference.
|
||||||
//
|
//
|
||||||
@@ -37,7 +41,6 @@ struct FWingProperty
|
|||||||
bool SetInt64(int64 I, WingOut Errors) const;
|
bool SetInt64(int64 I, WingOut Errors) const;
|
||||||
bool SetBool(bool B, WingOut Errors) const;
|
bool SetBool(bool B, WingOut Errors) const;
|
||||||
bool SetText(FString Value, 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
|
// Fetch a value. If an error occurs such as a type
|
||||||
// mismatch, returns an empty optional and prints an
|
// mismatch, returns an empty optional and prints an
|
||||||
@@ -72,14 +75,24 @@ struct FWingProperty
|
|||||||
//
|
//
|
||||||
// This gets the properties that are literally present in the
|
// This gets the properties that are literally present in the
|
||||||
// specified object or struct. No special interpretation is done.
|
// 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.
|
// Get all the visible properties of the specified object or struct.
|
||||||
//
|
//
|
||||||
// This gets the properties that have CPF_Edit marked on them.
|
// 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.
|
// 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);
|
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:
|
private:
|
||||||
static void StripEditable(TArray<FWingProperty> &Props);
|
|
||||||
static bool IsUnsigned(FNumericProperty* Prop);
|
static bool IsUnsigned(FNumericProperty* Prop);
|
||||||
static bool IsPinTypeProperty(FProperty *Prop);
|
static bool IsPinTypeProperty(FProperty *Prop);
|
||||||
void PrintExpectsReceived(const TCHAR *Type, WingOut Errors) const;
|
void PrintExpectsReceived(const TCHAR *Type, WingOut Errors) const;
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ public:
|
|||||||
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
|
||||||
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
|
||||||
|
|
||||||
/** Package a list of response texts into a single serialized JSON content-block array. */
|
|
||||||
static FString PackageResponses(const TArray<FString>& Responses);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static UWingServer* GWingServer;
|
static UWingServer* GWingServer;
|
||||||
|
|
||||||
@@ -76,14 +73,14 @@ private:
|
|||||||
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
|
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
|
||||||
TArray<FWingHandlerConfig> WingHandlerRegistry; // sorted by Name
|
TArray<FWingHandlerConfig> WingHandlerRegistry; // sorted by Name
|
||||||
void BuildWingHandlerRegistry();
|
void BuildWingHandlerRegistry();
|
||||||
void OnModulesChanged(FName ModuleName, EModuleChangeReason Reason);
|
FDelegateHandle LoadingPhasesCompleteHandle;
|
||||||
FDelegateHandle ModulesChangedHandle;
|
|
||||||
FWingHandlerConfig* FindHandler(const FString& Name);
|
FWingHandlerConfig* FindHandler(const FString& Name);
|
||||||
|
|
||||||
// Handle a complete JSON line and return the response JSON
|
// Handle a complete request and return the response bytes.
|
||||||
FString HandleRequest(const FString& Line);
|
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
|
||||||
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
|
void PreCallHandler();
|
||||||
void TryCallHandler(TSharedPtr<FJsonObject> Request);
|
FString PostCallHandler();
|
||||||
|
void TryCallHandler(TArrayView<const FString> Argv);
|
||||||
|
|
||||||
// ----- TCP server -----
|
// ----- TCP server -----
|
||||||
FSocket* ListenSocket = nullptr;
|
FSocket* ListenSocket = nullptr;
|
||||||
@@ -100,22 +97,23 @@ private:
|
|||||||
TArray<TSharedPtr<FClientConnection>> Clients;
|
TArray<TSharedPtr<FClientConnection>> Clients;
|
||||||
void AcceptNewConnections();
|
void AcceptNewConnections();
|
||||||
void CleanupFinishedClients();
|
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 void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
|
||||||
static bool ExtractRequestFromBuffer(
|
static bool ReceiveRequest(
|
||||||
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest);
|
FSocket* Socket, TArray<uint8>& OutRequest);
|
||||||
static bool ReceiveMoreBytesIntoBuffer(
|
|
||||||
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen);
|
|
||||||
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
|
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
|
||||||
static bool ProcessRequestOnGameThread(
|
static bool ProcessRequestOnGameThread(
|
||||||
const FString& Request, FString& Response);
|
const TArray<uint8>& Request, TArray<uint8>& Response);
|
||||||
static void WaitForAssetRegistry();
|
static void WaitForAssetRegistry();
|
||||||
|
|
||||||
// ----- The Critical Section -----
|
// ----- The Critical Section -----
|
||||||
struct FPendingMessage
|
struct FPendingMessage
|
||||||
{
|
{
|
||||||
FString Line;
|
TArray<uint8> Request;
|
||||||
TPromise<FString> Response;
|
TPromise<TArray<uint8>> Response;
|
||||||
FPendingMessage() : Response(TPromise<FString>()) {}
|
FPendingMessage() : Response(TPromise<TArray<uint8>>()) {}
|
||||||
};
|
};
|
||||||
FCriticalSection Mutex;
|
FCriticalSection Mutex;
|
||||||
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ public:
|
|||||||
// Empty the variable list.
|
// Empty the variable list.
|
||||||
void Empty() { Variables.Empty(); }
|
void Empty() { Variables.Empty(); }
|
||||||
|
|
||||||
|
// Add a variable.
|
||||||
|
void Add(const Var &Var) { Variables.Add(Var); }
|
||||||
|
|
||||||
// Return true if the variables are empty.
|
// Return true if the variables are empty.
|
||||||
bool IsEmpty() { return Variables.IsEmpty(); }
|
bool IsEmpty() { return Variables.IsEmpty(); }
|
||||||
|
|
||||||
@@ -72,21 +75,11 @@ public:
|
|||||||
// Check the sanity of the vars in the array. If allow
|
// Check the sanity of the vars in the array. If allow
|
||||||
// is false, then no variables are allowed in the array.
|
// is false, then no variables are allowed in the array.
|
||||||
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
|
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
|
class WingVariables
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using Var = WingVariableList::Var;
|
using Var = WingVariableList::Var;
|
||||||
WingVariables() {}
|
WingVariables() {}
|
||||||
|
|
||||||
@@ -125,6 +118,10 @@ public:
|
|||||||
|
|
||||||
void Print(WingOut Out);
|
void Print(WingOut Out);
|
||||||
|
|
||||||
|
// Parse variables.
|
||||||
|
|
||||||
|
bool Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors);
|
||||||
|
|
||||||
// Load: clear the workspace, then
|
// Load: clear the workspace, then
|
||||||
// copy everything from the backing store into the workspace.
|
// copy everything from the backing store into the workspace.
|
||||||
|
|
||||||
@@ -193,4 +190,8 @@ private:
|
|||||||
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
|
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
|
||||||
|
|
||||||
bool ErrorNoBackingStore(WingOut Errors);
|
bool ErrorNoBackingStore(WingOut Errors);
|
||||||
|
|
||||||
|
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
|
||||||
|
bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors);
|
||||||
|
WingVariableList *GetList(FName Name);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
MCP stdio-to-TCP bridge for UE Wingman.
|
|
||||||
|
|
||||||
Exposes a single MCP tool "unreal" that forwards JSON commands to the
|
|
||||||
UE Wingman TCP server in the Unreal Editor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import socket
|
|
||||||
|
|
||||||
HOST = "localhost"
|
|
||||||
PORT = 9851
|
|
||||||
CONNECT_TIMEOUT = 2
|
|
||||||
READ_TIMEOUT = 30
|
|
||||||
|
|
||||||
TOOL_DESCRIPTION = (
|
|
||||||
"Send a command to the Unreal Editor's UE Wingman plugin. "
|
|
||||||
"The 'command' field specifies which operation to perform; "
|
|
||||||
"additional fields are command-specific parameters. "
|
|
||||||
'Use {"command": "Documentation_Manual"} to get an overview. '
|
|
||||||
"If the editor is not running, the call will return an error; "
|
|
||||||
"just ask the user to start the editor and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
TOOL_SCHEMA = {
|
|
||||||
"name": "unreal",
|
|
||||||
"description": TOOL_DESCRIPTION,
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {"type": "string", "description": "The command to execute"},
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
"additionalProperties": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
|
||||||
"""Try to connect to the editor. Returns True on success."""
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(CONNECT_TIMEOUT)
|
|
||||||
s.connect((HOST, PORT))
|
|
||||||
s.settimeout(READ_TIMEOUT)
|
|
||||||
sock = s
|
|
||||||
return True
|
|
||||||
except (ConnectionRefusedError, socket.timeout, OSError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect():
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
try:
|
|
||||||
sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def send_and_receive(message):
|
|
||||||
"""Send a JSON message to the editor and return the null-terminated response."""
|
|
||||||
data = json.dumps(message) + "\0"
|
|
||||||
sock.sendall(data.encode())
|
|
||||||
|
|
||||||
result = b""
|
|
||||||
while True:
|
|
||||||
chunk = sock.recv(65536)
|
|
||||||
if not chunk:
|
|
||||||
raise ConnectionError("Connection closed")
|
|
||||||
result += chunk
|
|
||||||
if b"\0" in result:
|
|
||||||
break
|
|
||||||
|
|
||||||
return result[:result.index(b"\0")].decode()
|
|
||||||
|
|
||||||
|
|
||||||
def forward_to_editor(arguments):
|
|
||||||
"""Forward arguments to the editor, return the result dict."""
|
|
||||||
if not connect():
|
|
||||||
return {"error": "Unreal Editor is not running. Start the editor and try again."}
|
|
||||||
try:
|
|
||||||
return send_and_receive(arguments)
|
|
||||||
except Exception:
|
|
||||||
disconnect()
|
|
||||||
return {"error": "Lost connection to Unreal Editor."}
|
|
||||||
|
|
||||||
|
|
||||||
def make_jsonrpc(msg_id, result):
|
|
||||||
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_editor_response(result):
|
|
||||||
"""Parse and validate a raw editor response into an MCP content list.
|
|
||||||
|
|
||||||
MCP expects `content` to be a list of objects, each with at least a
|
|
||||||
string "type" field (e.g. {"type": "text", "text": "..."}). Anything
|
|
||||||
else is replaced with a single error item so the client sees a clear
|
|
||||||
message instead of a schema violation.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: invalid JSON."}]
|
|
||||||
if not isinstance(parsed, list):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: expected a list."}]
|
|
||||||
for item in parsed:
|
|
||||||
if not isinstance(item, dict):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: list item is not an object."}]
|
|
||||||
if not isinstance(item.get("type"), str):
|
|
||||||
return [{"type": "text", "text": "Malformed response from editor: item missing string 'type' field."}]
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
def handle_message(msg):
|
|
||||||
"""Handle one JSON-RPC message from Claude Code."""
|
|
||||||
msg_id = msg.get("id")
|
|
||||||
method = msg.get("method", "")
|
|
||||||
|
|
||||||
# Notifications don't get responses
|
|
||||||
if msg_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if method == "initialize":
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"protocolVersion": "2024-11-05",
|
|
||||||
"capabilities": {"tools": {}},
|
|
||||||
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
|
|
||||||
})
|
|
||||||
|
|
||||||
if method == "tools/list":
|
|
||||||
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
|
|
||||||
|
|
||||||
if method == "tools/call":
|
|
||||||
params = msg.get("params", {})
|
|
||||||
arguments = params.get("arguments", {})
|
|
||||||
result = forward_to_editor(arguments)
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
content = [{"type": "text", "text": result["error"]}]
|
|
||||||
else:
|
|
||||||
content = parse_editor_response(result)
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"content": content,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": msg_id,
|
|
||||||
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
for line in sys.stdin:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
msg = json.loads(line)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
response = handle_message(msg)
|
|
||||||
if response is not None:
|
|
||||||
sys.stdout.write(json.dumps(response) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,37 +1,23 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 ...]
|
Usage: ue-wingman.py <arg1> [arg2 ...]
|
||||||
|
|
||||||
Values starting with '[' or '{' are parsed as JSON.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
HOST = "localhost"
|
HOST = "localhost"
|
||||||
PORT = 9851
|
PORT = 9851
|
||||||
TIMEOUT = 120
|
TIMEOUT = 15
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
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 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(TIMEOUT)
|
sock.settimeout(TIMEOUT)
|
||||||
@@ -41,7 +27,14 @@ def main():
|
|||||||
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
|
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
|
||||||
sys.exit(1)
|
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""
|
result = b""
|
||||||
while True:
|
while True:
|
||||||
@@ -49,29 +42,9 @@ def main():
|
|||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
result += chunk
|
result += chunk
|
||||||
if b"\0" in result:
|
|
||||||
break
|
|
||||||
|
|
||||||
sock.close()
|
sock.close()
|
||||||
result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode()
|
print(result.decode(), end="")
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("Error: response is not valid JSON.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not isinstance(parsed, list):
|
|
||||||
print("Error: response is not a list of content blocks.")
|
|
||||||
sys.exit(1)
|
|
||||||
for block in parsed:
|
|
||||||
if not (isinstance(block, dict)
|
|
||||||
and block.get("type") == "text"
|
|
||||||
and isinstance(block.get("text"), str)):
|
|
||||||
print("Error: response contains a non-text block.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\n---\n".join(block["text"] for block in parsed))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "CoreUObject.h"
|
#include "CoreUObject.h"
|
||||||
#include "Containers/Deque.h"
|
#include "Containers/Deque.h"
|
||||||
|
|||||||
@@ -125,6 +125,21 @@ enum class ElxLuaSyntaxCheck : uint8 {
|
|||||||
InvalidLua,
|
InvalidLua,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// ElxControllerType
|
||||||
|
//
|
||||||
|
// The three types of controller recognized by luprex.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ElxControllerType : uint8 {
|
||||||
|
KeyboardMouse,
|
||||||
|
XboxGamepad,
|
||||||
|
PlayStationGamepad,
|
||||||
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Log Categories
|
// Log Categories
|
||||||
|
|||||||
@@ -317,6 +317,12 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co
|
|||||||
return Result;
|
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)
|
void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
|
||||||
{
|
{
|
||||||
// For throttled verbosity levels, suppress repeated messages with the
|
// For throttled verbosity levels, suppress repeated messages with the
|
||||||
@@ -338,8 +344,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
|
|||||||
|
|
||||||
// Generate the formatted string.
|
// Generate the formatted string.
|
||||||
//
|
//
|
||||||
FText InPatternText(FText::FromString(InPattern));
|
FText Message = FormatMessageInternal(InPattern, MoveTemp(InArgs));
|
||||||
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
|
|
||||||
FString MessageString = Message.ToString();
|
FString MessageString = Message.ToString();
|
||||||
|
|
||||||
// Get the blueprint name.
|
// Get the blueprint name.
|
||||||
|
|||||||
@@ -105,9 +105,16 @@ public:
|
|||||||
//
|
//
|
||||||
static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild);
|
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
|
// Format a message using FTextFormatter::Format, and send
|
||||||
// it to UE_LOG. The Context object's name is used as the
|
// it to UE_LOG. The Context object's name is used as the
|
||||||
// log category. Meant to be used internally by the Format
|
// log category. Meant to be used internally by the Format
|
||||||
// Log Message K2Node.
|
// Log Message K2Node.
|
||||||
//
|
//
|
||||||
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
|
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
|
|||||||
}
|
}
|
||||||
else
|
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.
|
// This is the node that does all the Format work and outputs the message.
|
||||||
|
|||||||
105
Source/Integration/InputDeviceTracker.cpp
Normal file
105
Source/Integration/InputDeviceTracker.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
#include "InputDeviceTracker.h"
|
||||||
|
#include "Framework/Application/SlateApplication.h"
|
||||||
|
#include "GenericPlatform/GenericApplicationMessageHandler.h"
|
||||||
|
#include "InputCoreTypes.h"
|
||||||
|
|
||||||
|
// Last observed device classification. Read with no
|
||||||
|
// synchronization; updates happen on the game thread
|
||||||
|
// from Slate's input pipeline, and stale reads on
|
||||||
|
// other threads are acceptable for this use case.
|
||||||
|
//
|
||||||
|
static ElxControllerType GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||||
|
|
||||||
|
// Keywords identifying a PlayStation-family gamepad.
|
||||||
|
// Matched case-insensitively against the InputDeviceName
|
||||||
|
// and HardwareDeviceIdentifier fields of the current
|
||||||
|
// FInputDeviceScope.
|
||||||
|
//
|
||||||
|
static const TCHAR* const PlaystationKeywords[] = {
|
||||||
|
TEXT("Playstation"),
|
||||||
|
TEXT("PS3"),
|
||||||
|
TEXT("PS4"),
|
||||||
|
TEXT("PS5"),
|
||||||
|
TEXT("PS6"),
|
||||||
|
TEXT("PS7"),
|
||||||
|
TEXT("Dualsense"),
|
||||||
|
TEXT("Dualshock"),
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool ContainsAnyPlaystationKeyword(const FString& Haystack)
|
||||||
|
{
|
||||||
|
for (const TCHAR* Keyword : PlaystationKeywords)
|
||||||
|
{
|
||||||
|
if (Haystack.Contains(Keyword, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classifies the active gamepad by scanning the current
|
||||||
|
// FInputDeviceScope. Defaults to Xbox; switches to
|
||||||
|
// PlayStation only on a keyword match.
|
||||||
|
//
|
||||||
|
ElxControllerType ClassifyGamepadFromScope()
|
||||||
|
{
|
||||||
|
const FInputDeviceScope* Scope = FInputDeviceScope::GetCurrent();
|
||||||
|
if (Scope != nullptr)
|
||||||
|
{
|
||||||
|
if (ContainsAnyPlaystationKeyword(Scope->InputDeviceName.ToString()) ||
|
||||||
|
ContainsAnyPlaystationKeyword(Scope->HardwareDeviceIdentifier))
|
||||||
|
{
|
||||||
|
return ElxControllerType::PlayStationGamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ElxControllerType::XboxGamepad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FInputDeviceTrackerProcessor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent)
|
||||||
|
{
|
||||||
|
if (KeyEvent.GetKey().IsGamepadKey())
|
||||||
|
{
|
||||||
|
GLastControllerType = ClassifyGamepadFromScope();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FInputDeviceTrackerProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
|
||||||
|
{
|
||||||
|
GLastControllerType = ElxControllerType::KeyboardMouse;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxInputDeviceTracker::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
if (FSlateApplication::IsInitialized())
|
||||||
|
{
|
||||||
|
Processor = MakeShared<FInputDeviceTrackerProcessor>();
|
||||||
|
FSlateApplication::Get().RegisterInputPreProcessor(Processor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxInputDeviceTracker::Deinitialize()
|
||||||
|
{
|
||||||
|
if (Processor.IsValid() && FSlateApplication::IsInitialized())
|
||||||
|
{
|
||||||
|
FSlateApplication::Get().UnregisterInputPreProcessor(Processor);
|
||||||
|
}
|
||||||
|
Processor.Reset();
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
ElxControllerType UlxInputDeviceTracker::GetLastControllerType()
|
||||||
|
{
|
||||||
|
return GLastControllerType;
|
||||||
|
}
|
||||||
64
Source/Integration/InputDeviceTracker.h
Normal file
64
Source/Integration/InputDeviceTracker.h
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// InputDeviceTracker.h
|
||||||
|
//
|
||||||
|
// Tracks the most recently used input device, classifying
|
||||||
|
// it as keyboard/mouse, Xbox gamepad, or PlayStation
|
||||||
|
// gamepad. The subsystem registers a Slate input
|
||||||
|
// preprocessor that watches button-down events; analog
|
||||||
|
// and mouse-move events are ignored. Read the current
|
||||||
|
// classification via the static accessor.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/GameInstanceSubsystem.h"
|
||||||
|
#include "Framework/Application/IInputProcessor.h"
|
||||||
|
#include "Common.h"
|
||||||
|
#include "InputDeviceTracker.generated.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// FInputDeviceTrackerProcessor
|
||||||
|
//
|
||||||
|
// Slate input preprocessor. Updates the device-class
|
||||||
|
// static on each button-down event. Never consumes
|
||||||
|
// events (always returns false).
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
class FInputDeviceTrackerProcessor : public IInputProcessor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
|
||||||
|
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent) override;
|
||||||
|
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// UlxInputDeviceTracker
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
UCLASS(MinimalAPI)
|
||||||
|
class UlxInputDeviceTracker : public UGameInstanceSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
|
||||||
|
// Returns the classification of the most recently used
|
||||||
|
// input device. Defaults to KeyboardMouse until the
|
||||||
|
// first gamepad button event is observed.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Luprex|Input")
|
||||||
|
static ElxControllerType GetLastControllerType();
|
||||||
|
|
||||||
|
private:
|
||||||
|
TSharedPtr<FInputDeviceTrackerProcessor> Processor;
|
||||||
|
};
|
||||||
@@ -31,6 +31,7 @@ public class Integration : ModuleRules
|
|||||||
"UMG",
|
"UMG",
|
||||||
"UMGEditor",
|
"UMGEditor",
|
||||||
"EditorSubsystem",
|
"EditorSubsystem",
|
||||||
|
"ApplicationCore",
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.Add("Slate");
|
PrivateDependencyModuleNames.Add("Slate");
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ void ALuprexGameModeBase::UpdateConsoleOutput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma optimize("", off)
|
|
||||||
void ALuprexGameModeBase::UpdateTangibles() {
|
void ALuprexGameModeBase::UpdateTangibles() {
|
||||||
double radius = 1000.0; // Hardwired for now.
|
double radius = 1000.0; // Hardwired for now.
|
||||||
using TanArray = UlxTangibleManager::TanArray;
|
using TanArray = UlxTangibleManager::TanArray;
|
||||||
@@ -132,8 +131,9 @@ void ALuprexGameModeBase::UpdatePossessedTangible() {
|
|||||||
|
|
||||||
void ALuprexGameModeBase::UpdateLuaSourceCode() {
|
void ALuprexGameModeBase::UpdateLuaSourceCode() {
|
||||||
FlxLockedWrapper lockedwrap;
|
FlxLockedWrapper lockedwrap;
|
||||||
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()))
|
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()) || ReloadSource)
|
||||||
{
|
{
|
||||||
|
ReloadSource = false;
|
||||||
drvutil::ostringstream srcpak;
|
drvutil::ostringstream srcpak;
|
||||||
FString LuprexRoot = FPaths::Combine(FPaths::ProjectDir(), TEXT("luprex"));
|
FString LuprexRoot = FPaths::Combine(FPaths::ProjectDir(), TEXT("luprex"));
|
||||||
std::string srcpakerr = drvutil::package_lua_source(TCHAR_TO_UTF8(*LuprexRoot), &srcpak);
|
std::string srcpakerr = drvutil::package_lua_source(TCHAR_TO_UTF8(*LuprexRoot), &srcpak);
|
||||||
@@ -259,4 +259,9 @@ ALuprexGameModeBase *ALuprexGameModeBase::FromContext(const UObject *context) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ALuprexGameModeBase::TriggerReloadSource(const UObject *WorldContextObject) {
|
||||||
|
ALuprexGameModeBase *GameMode = FromContext(WorldContextObject);
|
||||||
|
GameMode->ReloadSource = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ public:
|
|||||||
|
|
||||||
// Get the current Luprex Game Mode Base, given a Context object.
|
// Get the current Luprex Game Mode Base, given a Context object.
|
||||||
static ALuprexGameModeBase *FromContext(const UObject *Context);
|
static ALuprexGameModeBase *FromContext(const UObject *Context);
|
||||||
|
|
||||||
|
// Set the ReloadSource flag on the current Luprex game mode, causing
|
||||||
|
// the Lua source to be reloaded on the next tick.
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject"))
|
||||||
|
static void TriggerReloadSource(const UObject *WorldContextObject);
|
||||||
|
|
||||||
// The sensitivity level at which a log message triggers a debugger breakpoint.
|
// The sensitivity level at which a log message triggers a debugger breakpoint.
|
||||||
UPROPERTY(EditAnywhere, Category="Debugging Tools")
|
UPROPERTY(EditAnywhere, Category="Debugging Tools")
|
||||||
@@ -76,6 +81,9 @@ public:
|
|||||||
// This is always true unless you use the debugger to set it to false.
|
// This is always true unless you use the debugger to set it to false.
|
||||||
bool TickEnabled = true;
|
bool TickEnabled = true;
|
||||||
|
|
||||||
|
// True to trigger a source reload.
|
||||||
|
bool ReloadSource = false;
|
||||||
|
|
||||||
// Current Player ID
|
// Current Player ID
|
||||||
int64 PlayerId = 0;
|
int64 PlayerId = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "PlayerControllerBase.h"
|
#include "PlayerControllerBase.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
#include "GameFramework/InputDeviceSubsystem.h"
|
||||||
|
#include "GameFramework/InputSettings.h"
|
||||||
#include "RootCanvas.h"
|
#include "RootCanvas.h"
|
||||||
#include "Tangible.h"
|
#include "Tangible.h"
|
||||||
#include "TangibleManager.h"
|
#include "TangibleManager.h"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "Common.h"
|
||||||
#include "Engine/HitResult.h"
|
#include "Engine/HitResult.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "UObject/ObjectKey.h"
|
#include "UObject/ObjectKey.h"
|
||||||
|
|||||||
251
Source/Integration/PromptWidget.cpp
Normal file
251
Source/Integration/PromptWidget.cpp
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#include "PromptWidget.h"
|
||||||
|
#include "InputDeviceTracker.h"
|
||||||
|
#include "UtilityLibrary.h"
|
||||||
|
#include "PlayerControllerBase.h"
|
||||||
|
#include "InputAction.h"
|
||||||
|
#include "InputMappingContext.h"
|
||||||
|
#include "Widgets/SOverlay.h"
|
||||||
|
#include "Widgets/Images/SImage.h"
|
||||||
|
#include "Widgets/Text/STextBlock.h"
|
||||||
|
#include "Widgets/Layout/SScaleBox.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Atlas contents:
|
||||||
|
//
|
||||||
|
// Row 1: Keyboard Button no Glyph, Left Mouse, Middle Mouse, Right Mouse
|
||||||
|
// Row 2: Left Trigger, Right Trigger, Left Shoulder, Right Shoulder
|
||||||
|
// Row 3: XBFace Down, XBFace Left, XBFace Up, XBFace Right
|
||||||
|
// Row 4: PSFace Down, PSFace Left, PSFace Up, PSFace Right
|
||||||
|
// Row 5: Arrow Down, Arrow Left, Arrow Up, Arrow Right
|
||||||
|
// Row 6: DPad Down, DPad Left, DPad Up, DPad Right
|
||||||
|
// Row 7: Space Bar, unused, unused, unused
|
||||||
|
// Row 8: unused, unused, unused, unused.
|
||||||
|
|
||||||
|
|
||||||
|
int32 UlxPromptWidget::ChooseIcon(bool Playstation, FKey Key) const
|
||||||
|
{
|
||||||
|
// Mouse buttons (row 1)
|
||||||
|
if (Key == EKeys::LeftMouseButton) return 1;
|
||||||
|
if (Key == EKeys::MiddleMouseButton) return 2;
|
||||||
|
if (Key == EKeys::RightMouseButton) return 3;
|
||||||
|
|
||||||
|
// Gamepad triggers and shoulders (row 2)
|
||||||
|
if (Key == EKeys::Gamepad_LeftTrigger) return 4;
|
||||||
|
if (Key == EKeys::Gamepad_RightTrigger) return 5;
|
||||||
|
if (Key == EKeys::Gamepad_LeftShoulder) return 6;
|
||||||
|
if (Key == EKeys::Gamepad_RightShoulder) return 7;
|
||||||
|
|
||||||
|
// Gamepad Face buttons (rows 3 and 4)
|
||||||
|
if (Key == EKeys::Gamepad_FaceButton_Bottom) return Playstation ? 12 : 8;
|
||||||
|
if (Key == EKeys::Gamepad_FaceButton_Left) return Playstation ? 13 : 9;
|
||||||
|
if (Key == EKeys::Gamepad_FaceButton_Top) return Playstation ? 14 : 10;
|
||||||
|
if (Key == EKeys::Gamepad_FaceButton_Right) return Playstation ? 15 : 11;
|
||||||
|
|
||||||
|
// Arrow keys (row 5)
|
||||||
|
if (Key == EKeys::Down) return 16;
|
||||||
|
if (Key == EKeys::Left) return 17;
|
||||||
|
if (Key == EKeys::Up) return 18;
|
||||||
|
if (Key == EKeys::Right) return 19;
|
||||||
|
|
||||||
|
// Gamepad D-Pad (row 6)
|
||||||
|
if (Key == EKeys::Gamepad_DPad_Down) return 20;
|
||||||
|
if (Key == EKeys::Gamepad_DPad_Left) return 21;
|
||||||
|
if (Key == EKeys::Gamepad_DPad_Up) return 22;
|
||||||
|
if (Key == EKeys::Gamepad_DPad_Right) return 23;
|
||||||
|
|
||||||
|
// Space bar (row 7)
|
||||||
|
if (Key == EKeys::SpaceBar) return 24;
|
||||||
|
|
||||||
|
// No icon for this key. Use the blank icon with a label.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::ChooseAppearance(int32 &OutIcon, FString &OutGlyph)
|
||||||
|
{
|
||||||
|
FKey Key = (ControllerType == ElxControllerType::KeyboardMouse) ? KeyboardKey : GamepadKey;
|
||||||
|
OutIcon = ChooseIcon(ControllerType == ElxControllerType::PlayStationGamepad, Key);
|
||||||
|
if (OutIcon == 0) OutGlyph = Key.GetDisplayName().ToString();
|
||||||
|
else OutGlyph = TEXT("");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FBox2f UlxPromptWidget::GetIconUVs(int32 IconIndex)
|
||||||
|
{
|
||||||
|
const float ColWidth = 1.0f / 4.0f;
|
||||||
|
const float RowHeight = 1.0f / 8.0f;
|
||||||
|
int32 Col = IconIndex % 4;
|
||||||
|
int32 Row = IconIndex / 4;
|
||||||
|
float U = Col * ColWidth;
|
||||||
|
float V = Row * RowHeight;
|
||||||
|
return FBox2f(FVector2f(U, V), FVector2f(U + ColWidth, V + RowHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
FMargin UlxPromptWidget::GetScaledMargins() const
|
||||||
|
{
|
||||||
|
return FMargin(
|
||||||
|
GlyphMargins.Left * Size.X,
|
||||||
|
GlyphMargins.Top * Size.Y,
|
||||||
|
GlyphMargins.Right * Size.X,
|
||||||
|
GlyphMargins.Bottom * Size.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SynchronizeProperties()
|
||||||
|
{
|
||||||
|
Super::SynchronizeProperties();
|
||||||
|
if (!MyImage.IsValid()) return;
|
||||||
|
|
||||||
|
int32 Icon = 0;
|
||||||
|
FString Glyph;
|
||||||
|
ChooseAppearance(Icon, Glyph);
|
||||||
|
|
||||||
|
MyBrush.SetUVRegion(GetIconUVs(Icon));
|
||||||
|
MyImage->InvalidateImage();
|
||||||
|
MyBox->SetWidthOverride(Size.X);
|
||||||
|
MyBox->SetHeightOverride(Size.Y);
|
||||||
|
const float Extra = Depressed ? 0.05f : 0.0f;
|
||||||
|
const FMargin ExtraPadding(Extra * Size.X, Extra * Size.Y, Extra * Size.X, Extra * Size.Y);
|
||||||
|
MyImageSlot->SetPadding(ExtraPadding);
|
||||||
|
MyGlyphSlot->SetPadding(GetScaledMargins() + ExtraPadding);
|
||||||
|
MyGlyph->SetColorAndOpacity(GlyphColor);
|
||||||
|
|
||||||
|
if (!Glyph.IsEmpty())
|
||||||
|
{
|
||||||
|
MyGlyph->SetText(FText::FromString(Glyph));
|
||||||
|
MyGlyph->SetVisibility(EVisibility::HitTestInvisible);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MyGlyph->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetSize(FVector2D InSize)
|
||||||
|
{
|
||||||
|
if (Size != InSize)
|
||||||
|
{
|
||||||
|
Size = InSize;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetGlyphMargins(FMargin InMargins)
|
||||||
|
{
|
||||||
|
if (GlyphMargins != InMargins)
|
||||||
|
{
|
||||||
|
GlyphMargins = InMargins;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetGlyphColor(FLinearColor InColor)
|
||||||
|
{
|
||||||
|
if (GlyphColor != InColor)
|
||||||
|
{
|
||||||
|
GlyphColor = InColor;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetDepressed(bool InDepressed)
|
||||||
|
{
|
||||||
|
if (Depressed != InDepressed)
|
||||||
|
{
|
||||||
|
Depressed = InDepressed;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetKeys(FKey InGamepadKey, FKey InKeyboardKey)
|
||||||
|
{
|
||||||
|
if ((GamepadKey != InGamepadKey) || (KeyboardKey != InKeyboardKey))
|
||||||
|
{
|
||||||
|
GamepadKey = InGamepadKey;
|
||||||
|
KeyboardKey = InKeyboardKey;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction)
|
||||||
|
{
|
||||||
|
check(InputMappingContext);
|
||||||
|
check(EnhancedInputAction);
|
||||||
|
|
||||||
|
FKey NewFirstKey;
|
||||||
|
FKey NewGamepadKey;
|
||||||
|
FKey NewKeyboardKey;
|
||||||
|
|
||||||
|
for (const FEnhancedActionKeyMapping& Mapping : InputMappingContext->GetMappings())
|
||||||
|
{
|
||||||
|
if (Mapping.Action != EnhancedInputAction) continue;
|
||||||
|
FKey Key = Mapping.Key;
|
||||||
|
if (Key.IsDigital())
|
||||||
|
{
|
||||||
|
if (!NewFirstKey.IsValid()) NewFirstKey = Mapping.Key;
|
||||||
|
if (Key.IsTouch()) { /* not supported */ }
|
||||||
|
else if (Key.IsGesture()) { /* not supported */ }
|
||||||
|
else if (Key.IsGamepadKey()) { if (!NewGamepadKey.IsValid()) NewGamepadKey = Key; }
|
||||||
|
else { if (!NewKeyboardKey.IsValid()) NewKeyboardKey = Key; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NewGamepadKey.IsValid()) NewGamepadKey = NewFirstKey;
|
||||||
|
if (!NewKeyboardKey.IsValid()) NewKeyboardKey = NewFirstKey;
|
||||||
|
SetKeys(NewGamepadKey, NewKeyboardKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UlxPromptWidget::OnTick(float DeltaTime)
|
||||||
|
{
|
||||||
|
ElxControllerType Type = UlxInputDeviceTracker::GetLastControllerType();
|
||||||
|
if (ControllerType != Type)
|
||||||
|
{
|
||||||
|
ControllerType = Type;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<SWidget> UlxPromptWidget::RebuildWidget()
|
||||||
|
{
|
||||||
|
if (!IsDesignTime())
|
||||||
|
{
|
||||||
|
ControllerType = UlxInputDeviceTracker::GetLastControllerType();
|
||||||
|
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UlxPromptWidget::OnTick));
|
||||||
|
}
|
||||||
|
|
||||||
|
MyBrush.SetResourceObject(ButtonAtlas.Get());
|
||||||
|
|
||||||
|
SAssignNew(MyImage, SImage).Image(&MyBrush);
|
||||||
|
|
||||||
|
SAssignNew(MyGlyph, STextBlock);
|
||||||
|
SAssignNew(MyScaleBox, SScaleBox)
|
||||||
|
.Stretch(EStretch::ScaleToFit)
|
||||||
|
[ MyGlyph.ToSharedRef() ];
|
||||||
|
|
||||||
|
MyOverlay = SNew(SOverlay);
|
||||||
|
MyOverlay->AddSlot().Expose(MyImageSlot)
|
||||||
|
[ MyImage.ToSharedRef() ];
|
||||||
|
MyOverlay->AddSlot().HAlign(HAlign_Fill).VAlign(VAlign_Fill).Expose(MyGlyphSlot)
|
||||||
|
[ MyScaleBox.ToSharedRef() ];
|
||||||
|
|
||||||
|
SAssignNew(MyBox, SBox)
|
||||||
|
[ MyOverlay.ToSharedRef() ];
|
||||||
|
|
||||||
|
SynchronizeProperties();
|
||||||
|
return MyBox.ToSharedRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UlxPromptWidget::ReleaseSlateResources(bool bReleaseChildren)
|
||||||
|
{
|
||||||
|
Super::ReleaseSlateResources(bReleaseChildren);
|
||||||
|
if (!IsDesignTime())
|
||||||
|
{
|
||||||
|
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
||||||
|
}
|
||||||
|
MyBox.Reset();
|
||||||
|
MyOverlay.Reset();
|
||||||
|
MyImage.Reset();
|
||||||
|
MyScaleBox.Reset();
|
||||||
|
MyGlyph.Reset();
|
||||||
|
MyImageSlot = nullptr;
|
||||||
|
MyGlyphSlot = nullptr;
|
||||||
|
}
|
||||||
89
Source/Integration/PromptWidget.h
Normal file
89
Source/Integration/PromptWidget.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Components/Widget.h"
|
||||||
|
#include "InputCoreTypes.h"
|
||||||
|
#include "Widgets/SOverlay.h"
|
||||||
|
#include "Widgets/Layout/SBox.h"
|
||||||
|
#include "Widgets/Layout/SScaleBox.h"
|
||||||
|
#include "Containers/Ticker.h"
|
||||||
|
#include "PromptWidget.generated.h"
|
||||||
|
|
||||||
|
class UInputAction;
|
||||||
|
class UInputMappingContext;
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS(BlueprintType, Blueprintable)
|
||||||
|
class INTEGRATION_API UlxPromptWidget : public UWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||||
|
TObjectPtr<UTexture2D> ButtonAtlas;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||||
|
ElxControllerType ControllerType = ElxControllerType::KeyboardMouse;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||||
|
FKey GamepadKey = EKeys::Gamepad_FaceButton_Left;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="Prompt")
|
||||||
|
FKey KeyboardKey = EKeys::Z;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||||
|
FVector2D Size = FVector2D(64, 64);
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||||
|
FMargin GlyphMargins = FMargin(0.0f, 0.1f, 0.0f, 0.1f);
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||||
|
FLinearColor GlyphColor = FLinearColor::White;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
|
||||||
|
bool Depressed = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetKeys(FKey InGamepadKey, FKey InKeyboardKey);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetGlyphMargins(FMargin InMargins);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetGlyphColor(FLinearColor InColor);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetSize(FVector2D InSize);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Prompt")
|
||||||
|
void SetDepressed(bool InDepressed);
|
||||||
|
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||||
|
virtual void SynchronizeProperties() override;
|
||||||
|
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FSlateBrush MyBrush;
|
||||||
|
TSharedPtr<SBox> MyBox;
|
||||||
|
TSharedPtr<SOverlay> MyOverlay;
|
||||||
|
TSharedPtr<SImage> MyImage;
|
||||||
|
TSharedPtr<SScaleBox> MyScaleBox;
|
||||||
|
TSharedPtr<STextBlock> MyGlyph;
|
||||||
|
SOverlay::FOverlaySlot* MyImageSlot = nullptr;
|
||||||
|
SOverlay::FOverlaySlot* MyGlyphSlot = nullptr;
|
||||||
|
|
||||||
|
FBox2f GetIconUVs(int32 IconIndex);
|
||||||
|
FMargin GetScaledMargins() const;
|
||||||
|
int32 ChooseIcon(bool Playstation, FKey Key) const;
|
||||||
|
void ChooseAppearance(int32 &OutIcon, FString &OutGlyph);
|
||||||
|
bool OnTick(float DeltaTime);
|
||||||
|
|
||||||
|
FTSTicker::FDelegateHandle TickHandle;
|
||||||
|
};
|
||||||
398
Source/Integration/RadialMenu.cpp
Normal file
398
Source/Integration/RadialMenu.cpp
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
#include "RadialMenu.h"
|
||||||
|
#include "Rendering/DrawElements.h"
|
||||||
|
#include "Engine/Texture2D.h"
|
||||||
|
#include "Fonts/FontMeasure.h"
|
||||||
|
#include "Framework/Application/SlateApplication.h"
|
||||||
|
#include "Styling/SlateBrush.h"
|
||||||
|
#include "Widgets/SLeafWidget.h"
|
||||||
|
|
||||||
|
|
||||||
|
TArray<FRadialMenuItem> FRadialMenuItem::Calculate(const FRadialMenuConfig &Config)
|
||||||
|
{
|
||||||
|
TArray<FRadialMenuItem> Items;
|
||||||
|
const int32 NumItems = Config.MenuItems.Num();
|
||||||
|
if (NumItems <= 0) return Items;
|
||||||
|
|
||||||
|
int32 NumRight = (NumItems / 2);
|
||||||
|
int32 NumLeft = NumItems - NumRight;
|
||||||
|
Items.SetNum(NumItems);
|
||||||
|
|
||||||
|
// Measure each non-empty label first; the spoke layout's item height
|
||||||
|
// is derived from the tallest label.
|
||||||
|
const TSharedRef<FSlateFontMeasure> FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
||||||
|
double MaxTextHeight = 5.0;
|
||||||
|
for (int32 I = 0; I < NumItems; I++)
|
||||||
|
{
|
||||||
|
if (Config.MenuItems[I].IsEmpty()) continue;
|
||||||
|
Items[I].TextSize = FontMeasure->Measure(Config.MenuItems[I], Config.Font);
|
||||||
|
MaxTextHeight = FMath::Max(MaxTextHeight, Items[I].TextSize.Y);
|
||||||
|
}
|
||||||
|
const float ItemHeight = static_cast<float>(MaxTextHeight * 1.2);
|
||||||
|
|
||||||
|
View LeftItems(Items.GetData(), NumLeft);
|
||||||
|
View RightItems(Items.GetData() + NumLeft, NumRight);
|
||||||
|
|
||||||
|
CalculateSpokes(LeftItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
|
||||||
|
CalculateSpokes(RightItems, NumItems, ItemHeight, Config.InnerRadius, Config.MinSpoke);
|
||||||
|
double LeftWidth = WidestSpoke(LeftItems);
|
||||||
|
double RightWidth = WidestSpoke(RightItems);
|
||||||
|
double HalfWidth = FMath::Max(LeftWidth, RightWidth) + Config.Spread;
|
||||||
|
CalculateSpread(LeftItems, HalfWidth - LeftWidth);
|
||||||
|
CalculateSpread(RightItems, HalfWidth - RightWidth);
|
||||||
|
|
||||||
|
FlipHorizontal(LeftItems);
|
||||||
|
return Items;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector2D> FRadialMenuItem::CalculateDirections(int32 NumItems)
|
||||||
|
{
|
||||||
|
TArray<FVector2D> Result;
|
||||||
|
if (NumItems <= 0) return Result;
|
||||||
|
|
||||||
|
const int32 NumRight = (NumItems / 2);
|
||||||
|
const int32 NumLeft = NumItems - NumRight;
|
||||||
|
Result.SetNum(NumItems);
|
||||||
|
|
||||||
|
// Left side: SpokeVector with X flipped (matches FlipHorizontal in Calculate).
|
||||||
|
for (int32 I = 0; I < NumLeft; I++)
|
||||||
|
{
|
||||||
|
FVector2D V = SpokeVector(I, NumLeft, NumItems);
|
||||||
|
V.X = -V.X;
|
||||||
|
Result[I] = V;
|
||||||
|
}
|
||||||
|
for (int32 I = 0; I < NumRight; I++)
|
||||||
|
{
|
||||||
|
Result[NumLeft + I] = SpokeVector(I, NumRight, NumItems);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D FRadialMenuItem::SpokeVector(int32 I, int32 NSide, int32 NTotal)
|
||||||
|
{
|
||||||
|
double SpokeAngle = 1.0 / NTotal;
|
||||||
|
double OffsetAngle = 0.5 * (0.5 - ((NSide - 1) * SpokeAngle));
|
||||||
|
double Revolutions = (I * SpokeAngle) + OffsetAngle;
|
||||||
|
double Radians = (Revolutions * 2.0 * UE_PI);
|
||||||
|
return FVector2D(FMath::Sin(Radians), -FMath::Cos(Radians));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FRadialMenuItem::FlipHorizontal(View V)
|
||||||
|
{
|
||||||
|
for (FRadialMenuItem& Item : V)
|
||||||
|
{
|
||||||
|
Item.Point1.X = -Item.Point1.X;
|
||||||
|
Item.Point2.X = -Item.Point2.X;
|
||||||
|
Item.Point3.X = -Item.Point3.X;
|
||||||
|
Item.RightSide = !Item.RightSide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double FRadialMenuItem::WidestSpoke(View V)
|
||||||
|
{
|
||||||
|
double Result = 0.0;
|
||||||
|
for (const FRadialMenuItem &Item : V)
|
||||||
|
{
|
||||||
|
Result = FMath::Max(Item.Point2.X, Result);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FRadialMenuItem::CalculateSpread(View V, double Offset)
|
||||||
|
{
|
||||||
|
for (FRadialMenuItem &Item : V)
|
||||||
|
{
|
||||||
|
Item.Point3 = Item.Point2 + FVector2D(Offset, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FRadialMenuItem::CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke)
|
||||||
|
{
|
||||||
|
if (V.Num() == 0) return;
|
||||||
|
|
||||||
|
// RightSide is always initialized to true, it may get
|
||||||
|
// reversed by FlipHorizontal.
|
||||||
|
for (int32 I = 0; I < V.Num(); I++)
|
||||||
|
{
|
||||||
|
V[I].RightSide = true;
|
||||||
|
V[I].Point1 = SpokeVector(I, V.Num(), TotalItems) * InnerRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate point2 for all spokes.
|
||||||
|
double NextLineMin = ItemHeight * 0.5;
|
||||||
|
int32 Mid = (V.Num() / 2);
|
||||||
|
if (V.Num() & 1)
|
||||||
|
{
|
||||||
|
V[Mid].Point2 = FVector2D(InnerRadius + MinSpoke, 0.0);
|
||||||
|
NextLineMin = ItemHeight;
|
||||||
|
Mid += 1;
|
||||||
|
}
|
||||||
|
for (int32 I = Mid; I < V.Num(); I++)
|
||||||
|
{
|
||||||
|
FVector2D UnitVec = SpokeVector(I, V.Num(), TotalItems);
|
||||||
|
double Y = (UnitVec.Y * (InnerRadius + MinSpoke));
|
||||||
|
if (Y < NextLineMin) Y = NextLineMin;
|
||||||
|
NextLineMin = Y + ItemHeight;
|
||||||
|
FVector2D Point2 = UnitVec * (Y / UnitVec.Y);
|
||||||
|
V[I].Point2 = Point2;
|
||||||
|
V[V.Num() - 1 - I].Point2 = Point2 * FVector2D(1.0,-1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The middle spoke is calculated using a different formula,
|
||||||
|
// which may result in a short spoke. If so, fix it to make
|
||||||
|
// it at least as long as the adjacent spoke.
|
||||||
|
if ((V.Num() & 1) && (V.Num() >= 3))
|
||||||
|
{
|
||||||
|
Mid = V.Num() / 2;
|
||||||
|
if (V[Mid].Point2.X < V[Mid + 1].Point2.X)
|
||||||
|
V[Mid].Point2.X = V[Mid + 1].Point2.X;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Slate widget that paints the radial menu's polylines.
|
||||||
|
class SRadialMenu : public SLeafWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SLATE_BEGIN_ARGS(SRadialMenu) {}
|
||||||
|
SLATE_END_ARGS()
|
||||||
|
|
||||||
|
void Construct(const FArguments& InArgs, const FRadialMenuConfig& InConfig)
|
||||||
|
{
|
||||||
|
Config = InConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetConfig(const FRadialMenuConfig& NewConfig)
|
||||||
|
{
|
||||||
|
Config = NewConfig;
|
||||||
|
Items = FRadialMenuItem::Calculate(Config);
|
||||||
|
Invalidate(EInvalidateWidgetReason::Layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPointer(FVector2D NewPointer)
|
||||||
|
{
|
||||||
|
if (PointerVector == NewPointer) return;
|
||||||
|
PointerVector = NewPointer;
|
||||||
|
Invalidate(EInvalidateWidgetReason::Paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual FVector2D ComputeDesiredSize(float) const override
|
||||||
|
{
|
||||||
|
FVector2D Min(0.0, 0.0);
|
||||||
|
FVector2D Max(0.0, 0.0);
|
||||||
|
for (const FRadialMenuItem &Item : Items)
|
||||||
|
{
|
||||||
|
for (const FVector2D &P : { Item.Point1, Item.Point2, Item.Point3 })
|
||||||
|
{
|
||||||
|
Min.X = FMath::Min(Min.X, P.X);
|
||||||
|
Min.Y = FMath::Min(Min.Y, P.Y);
|
||||||
|
Max.X = FMath::Max(Max.X, P.X);
|
||||||
|
Max.Y = FMath::Max(Max.Y, P.Y);
|
||||||
|
}
|
||||||
|
if (Item.TextSize.X <= 0) continue;
|
||||||
|
const double Gap = Config.Spread * 0.5;
|
||||||
|
const double TextLeft = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
|
||||||
|
const double TextRight = Item.RightSide ? (Item.Point3.X + Gap + Item.TextSize.X) : (Item.Point3.X - Gap);
|
||||||
|
Min.X = FMath::Min(Min.X, TextLeft);
|
||||||
|
Max.X = FMath::Max(Max.X, TextRight);
|
||||||
|
Min.Y = FMath::Min(Min.Y, Item.Point3.Y - Item.TextSize.Y * 0.5);
|
||||||
|
Max.Y = FMath::Max(Max.Y, Item.Point3.Y + Item.TextSize.Y * 0.5);
|
||||||
|
}
|
||||||
|
// Symmetric around the origin so the menu draws centered.
|
||||||
|
double HalfW = FMath::Max(FMath::Abs(Min.X), FMath::Abs(Max.X));
|
||||||
|
double HalfH = FMath::Max(FMath::Abs(Min.Y), FMath::Abs(Max.Y));
|
||||||
|
return FVector2D(HalfW * 2.0, HalfH * 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
|
||||||
|
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
|
||||||
|
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
|
||||||
|
{
|
||||||
|
const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5;
|
||||||
|
const FPaintGeometry PaintGeom = AllottedGeometry.ToPaintGeometry();
|
||||||
|
|
||||||
|
for (int32 I = 0; I < Items.Num(); I++)
|
||||||
|
{
|
||||||
|
const FRadialMenuItem& Item = Items[I];
|
||||||
|
const bool bSelected = (I == Config.SelectedItem);
|
||||||
|
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||||
|
const float Thickness = bSelected ? Config.SelectedThickness : Config.LineThickness;
|
||||||
|
TArray<FVector2D> Points;
|
||||||
|
Points.Add(Center + Item.Point1);
|
||||||
|
Points.Add(Center + Item.Point2);
|
||||||
|
Points.Add(Center + Item.Point3);
|
||||||
|
FSlateDrawElement::MakeLines(OutDrawElements, LayerId, PaintGeom, Points, ESlateDrawEffect::None, Color, true, Thickness);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw item labels, vertically centered on Point3. Right-side items
|
||||||
|
// are left-aligned to Point3; left-side items are right-aligned.
|
||||||
|
for (int32 I = 0; I < Items.Num(); I++)
|
||||||
|
{
|
||||||
|
const FRadialMenuItem& Item = Items[I];
|
||||||
|
if (Item.TextSize.X <= 0) continue;
|
||||||
|
const bool bSelected = (I == Config.SelectedItem);
|
||||||
|
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||||
|
const double Gap = Config.Spread * 0.5;
|
||||||
|
FVector2D TextPos;
|
||||||
|
TextPos.X = Item.RightSide ? (Item.Point3.X + Gap) : (Item.Point3.X - Gap - Item.TextSize.X);
|
||||||
|
TextPos.Y = Item.Point3.Y - Item.TextSize.Y * 0.5;
|
||||||
|
FSlateDrawElement::MakeText(
|
||||||
|
OutDrawElements,
|
||||||
|
LayerId + 1,
|
||||||
|
AllottedGeometry.ToPaintGeometry(Item.TextSize, FSlateLayoutTransform(Center + TextPos)),
|
||||||
|
Config.MenuItems[I],
|
||||||
|
Config.Font,
|
||||||
|
ESlateDrawEffect::None,
|
||||||
|
Color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.DotRadius > 0.0f && Config.DotTexture != nullptr)
|
||||||
|
{
|
||||||
|
FSlateBrush DotBrush;
|
||||||
|
DotBrush.SetResourceObject(Config.DotTexture);
|
||||||
|
const FVector2D DotSize(Config.DotRadius * 2.0f);
|
||||||
|
for (int32 I = 0; I < Items.Num(); I++)
|
||||||
|
{
|
||||||
|
const FRadialMenuItem& Item = Items[I];
|
||||||
|
const bool bSelected = (I == Config.SelectedItem);
|
||||||
|
const FLinearColor& Color = bSelected ? Config.SelectedColor : Config.ItemColor;
|
||||||
|
for (const FVector2D& Point : { Item.Point1, Item.Point3 })
|
||||||
|
{
|
||||||
|
const FVector2D LocalPoint = Center + Point;
|
||||||
|
FSlateDrawElement::MakeBox(
|
||||||
|
OutDrawElements,
|
||||||
|
LayerId + 1,
|
||||||
|
AllottedGeometry.ToPaintGeometry(DotSize, FSlateLayoutTransform(LocalPoint - FVector2D(Config.DotRadius))),
|
||||||
|
&DotBrush,
|
||||||
|
ESlateDrawEffect::None,
|
||||||
|
Color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teardrop: visualizes PointerVector inside InnerRadius. The texture's
|
||||||
|
// tip naturally points up (0,-1); rotate it to align with PointerVector.
|
||||||
|
// When PointerVector is (0,0), the teardrop stays upright at the center.
|
||||||
|
if (Config.TeardropRadius > 0.0f && Config.Teardrop != nullptr)
|
||||||
|
{
|
||||||
|
FSlateBrush TeardropBrush;
|
||||||
|
TeardropBrush.SetResourceObject(Config.Teardrop);
|
||||||
|
const FVector2D TeardropSize(Config.TeardropRadius * 2.0f);
|
||||||
|
const FVector2D TeardropCenter = Center + PointerVector * Config.InnerRadius;
|
||||||
|
const float Angle = PointerVector.IsZero()
|
||||||
|
? 0.0f
|
||||||
|
: static_cast<float>(FMath::Atan2(PointerVector.X, -PointerVector.Y));
|
||||||
|
FSlateDrawElement::MakeRotatedBox(
|
||||||
|
OutDrawElements,
|
||||||
|
LayerId + 1,
|
||||||
|
AllottedGeometry.ToPaintGeometry(TeardropSize, FSlateLayoutTransform(TeardropCenter - FVector2D(Config.TeardropRadius))),
|
||||||
|
&TeardropBrush,
|
||||||
|
ESlateDrawEffect::None,
|
||||||
|
Angle);
|
||||||
|
}
|
||||||
|
return LayerId + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FRadialMenuConfig Config;
|
||||||
|
|
||||||
|
TArray<FRadialMenuItem> Items;
|
||||||
|
|
||||||
|
FVector2D PointerVector = {0,0};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void URadialMenuWidget::SetMenuItems(const TArray<FString>& NewMenuItems)
|
||||||
|
{
|
||||||
|
if (Config.MenuItems == NewMenuItems) return;
|
||||||
|
|
||||||
|
Config.MenuItems = NewMenuItems;
|
||||||
|
Config.SelectedItem = -1;
|
||||||
|
PointerVector = FVector2D();
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
FString URadialMenuWidget::GetSelectedMenuItem() const
|
||||||
|
{
|
||||||
|
if (!Config.MenuItems.IsValidIndex(Config.SelectedItem)) return FString();
|
||||||
|
return Config.MenuItems[Config.SelectedItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
void URadialMenuWidget::SetSelectedItem(int32 NewSelectedItem)
|
||||||
|
{
|
||||||
|
if (Config.SelectedItem == NewSelectedItem) return;
|
||||||
|
|
||||||
|
Config.SelectedItem = NewSelectedItem;
|
||||||
|
SynchronizeProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
void URadialMenuWidget::SynchronizeProperties()
|
||||||
|
{
|
||||||
|
Super::SynchronizeProperties();
|
||||||
|
|
||||||
|
if (Directions.Num() != Config.MenuItems.Num())
|
||||||
|
{
|
||||||
|
Directions = FRadialMenuItem::CalculateDirections(Config.MenuItems.Num());
|
||||||
|
}
|
||||||
|
if (MySlateWidget.IsValid())
|
||||||
|
{
|
||||||
|
MySlateWidget->SetConfig(Config);
|
||||||
|
MySlateWidget->SetPointer(PointerVector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void URadialMenuWidget::AddPointer(FVector2D Direction, float Scale)
|
||||||
|
{
|
||||||
|
SetPointer(PointerVector + (Direction * Scale), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void URadialMenuWidget::SetPointer(FVector2D Direction, float Scale)
|
||||||
|
{
|
||||||
|
if ((!IsVisible()) || (Directions.Num() == 0))
|
||||||
|
{
|
||||||
|
PointerVector = FVector2D();
|
||||||
|
SetSelectedItem(-1);
|
||||||
|
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PointerVector = (Direction * Scale);
|
||||||
|
|
||||||
|
if (PointerVector.Length() < 0.75)
|
||||||
|
{
|
||||||
|
SetSelectedItem(-1);
|
||||||
|
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PointerVector.Length() > 1.0)
|
||||||
|
{
|
||||||
|
PointerVector.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 BestIndex = -1;
|
||||||
|
double BestDot = -2.0;
|
||||||
|
for (int32 I = 0; I < Directions.Num(); I++)
|
||||||
|
{
|
||||||
|
FVector2D SpokeDir = Directions[I];
|
||||||
|
if (!SpokeDir.Normalize()) continue;
|
||||||
|
double Dot = FVector2D::DotProduct(SpokeDir, PointerVector);
|
||||||
|
if (Dot <= BestDot) continue;
|
||||||
|
BestDot = Dot;
|
||||||
|
BestIndex = I;
|
||||||
|
}
|
||||||
|
SetSelectedItem(BestIndex);
|
||||||
|
if (MySlateWidget.IsValid()) MySlateWidget->SetPointer(PointerVector);
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<SWidget> URadialMenuWidget::RebuildWidget()
|
||||||
|
{
|
||||||
|
MySlateWidget = SNew(SRadialMenu, Config);
|
||||||
|
return MySlateWidget.ToSharedRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
void URadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren)
|
||||||
|
{
|
||||||
|
Super::ReleaseSlateResources(bReleaseChildren);
|
||||||
|
MySlateWidget.Reset();
|
||||||
|
}
|
||||||
164
Source/Integration/RadialMenu.h
Normal file
164
Source/Integration/RadialMenu.h
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
//
|
||||||
|
// This class implements the layout calculatations for a radial
|
||||||
|
// menu.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/Widget.h"
|
||||||
|
#include "Fonts/SlateFontInfo.h"
|
||||||
|
#include "RadialMenu.generated.h"
|
||||||
|
|
||||||
|
class SRadialMenu;
|
||||||
|
class UTexture2D;
|
||||||
|
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FRadialMenuConfig
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
TArray<FString> MenuItems;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
FSlateFontInfo Font;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float InnerRadius = 20.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float MinSpoke = 20.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float Spread = 20.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float LineThickness = 4.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
FLinearColor ItemColor = FLinearColor::White;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
TObjectPtr<UTexture2D> DotTexture;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float DotRadius = 3.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
TObjectPtr<UTexture2D> Teardrop;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float TeardropRadius = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
int32 SelectedItem = -1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
FLinearColor SelectedColor = FLinearColor::Yellow;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, Category="RadialMenu")
|
||||||
|
float SelectedThickness = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FRadialMenuItem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly)
|
||||||
|
FVector2D Point1 = {0,0};
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly)
|
||||||
|
FVector2D Point2 = {0,0};
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly)
|
||||||
|
FVector2D Point3 = {0,0};
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly)
|
||||||
|
bool RightSide = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly)
|
||||||
|
FVector2D TextSize = {0,0};
|
||||||
|
|
||||||
|
static TArray<FRadialMenuItem> Calculate(const FRadialMenuConfig &Config);
|
||||||
|
|
||||||
|
// Like Calculate, but only produces the unit-vector direction of
|
||||||
|
// each spoke. Cheaper to evaluate when only the spoke directions
|
||||||
|
// are needed (e.g. for hit-testing a pointer against the wheel).
|
||||||
|
static TArray<FVector2D> CalculateDirections(int32 NumItems);
|
||||||
|
|
||||||
|
private:
|
||||||
|
using View = TArrayView<FRadialMenuItem>;
|
||||||
|
|
||||||
|
// Give the unit vector for the selected spoke. NSide is
|
||||||
|
// the number of spokes on the side of the wheel that
|
||||||
|
// we're calculating, and NTotal is the total number of
|
||||||
|
// spokes on both sides. The spokes on a given side are
|
||||||
|
// organized top-to-bottom, and the angle between the
|
||||||
|
// spokes is always equal to (1/NTotal) of the circle.
|
||||||
|
static FVector2D SpokeVector(int32 I, int32 NSide, int32 NTotal);
|
||||||
|
|
||||||
|
// Populate Point1 and Point2, these are the
|
||||||
|
// endpoints of the spoke segment. Spokes are
|
||||||
|
// designed to always be long enough to make room
|
||||||
|
// for MinSpoke, but also to make enough room to
|
||||||
|
// keep the menu items from overlapping.
|
||||||
|
static void CalculateSpokes(View V, int32 TotalItems, float ItemHeight, float InnerRadius, float MinSpoke);
|
||||||
|
|
||||||
|
// Search for the widest spoke, and return its X coordinate.
|
||||||
|
static double WidestSpoke(View V);
|
||||||
|
|
||||||
|
// Populate Point3, this is the endpoint of the spread
|
||||||
|
// line that goes horizontal.
|
||||||
|
static void CalculateSpread(View V, double Offset);
|
||||||
|
|
||||||
|
// Flip everything in the specified view horizontally.
|
||||||
|
static void FlipHorizontal(View V);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS(BlueprintType, Blueprintable)
|
||||||
|
class URadialMenuWidget : public UWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetMenuItems(const TArray<FString>& NewMenuItems);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void ClearMenuItems() { SetMenuItems({}); }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetSelectedItem(int32 NewSelectedItem);
|
||||||
|
|
||||||
|
// Returns true if the selected item changed.
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void AddPointer(FVector2D Direction, float Scale);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
void SetPointer(FVector2D Direction, float Scale);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
FVector2D GetPointer() const { return PointerVector; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
FString GetSelectedMenuItem() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(EditAnywhere)
|
||||||
|
FRadialMenuConfig Config;
|
||||||
|
|
||||||
|
TArray<FVector2D> Directions;
|
||||||
|
|
||||||
|
FVector2D PointerVector = {0,0};
|
||||||
|
|
||||||
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||||
|
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||||
|
virtual void SynchronizeProperties() override;
|
||||||
|
|
||||||
|
TSharedPtr<SRadialMenu> MySlateWidget;
|
||||||
|
};
|
||||||
226
Source/Integration/ReadSlashCommand.cpp
Normal file
226
Source/Integration/ReadSlashCommand.cpp
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#include "ReadSlashCommand.h"
|
||||||
|
|
||||||
|
#include "BlueprintActionDatabaseRegistrar.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "KismetCompiler.h"
|
||||||
|
#include "LuaCall.h"
|
||||||
|
#include "SlashCommand.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "ReadSlashCommand"
|
||||||
|
|
||||||
|
const FName UK2Node_ReadSlashCommand::PrototypePinName(TEXT("Prototype"));
|
||||||
|
const FName UK2Node_ReadSlashCommand::InputValuesPinName(TEXT("Input Values"));
|
||||||
|
const FName UK2Node_ReadSlashCommand::ErrorPinName(TEXT("Error"));
|
||||||
|
|
||||||
|
bool UK2Node_ReadSlashCommand::ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps)
|
||||||
|
{
|
||||||
|
TArray<FString> Words;
|
||||||
|
Prototype.ParseIntoArrayWS(Words);
|
||||||
|
|
||||||
|
// The command name must be a slash followed by alphanumerics.
|
||||||
|
if (Words.Num() == 0 || !UlxSlashCommand::IsSlashCommand(Words[0]))
|
||||||
|
{
|
||||||
|
SetErrorMsg(TEXT("The prototype must start with a command name, e.g. \"/foo\"."));
|
||||||
|
Steps.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Steps.SetNum(Words.Num());
|
||||||
|
Steps[0].Word = Words[0];
|
||||||
|
TSet<FName> UsedNames;
|
||||||
|
|
||||||
|
for (int32 i = 1; i < Words.Num(); i++)
|
||||||
|
{
|
||||||
|
const FString& Word = Words[i];
|
||||||
|
FString FuncName = FString(TEXT("Read")) + Word;
|
||||||
|
UFunction* ReadFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(FName(*FuncName));
|
||||||
|
if (ReadFunc == nullptr)
|
||||||
|
{
|
||||||
|
SetErrorMsg(FString::Printf(TEXT("Unknown value type: %s"), *Word));
|
||||||
|
Steps.Empty();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FName PinName = AddPrefix(Word, 'R');
|
||||||
|
for (int Suffix = 2; UsedNames.Contains(PinName); Suffix++)
|
||||||
|
{
|
||||||
|
PinName = AddPrefix(FString::Printf(TEXT("%s%d"), *Word, Suffix), 'R');
|
||||||
|
}
|
||||||
|
Steps[i].Word = Word;
|
||||||
|
Steps[i].PinName = PinName;
|
||||||
|
Steps[i].ReadFunction = ReadFunc;
|
||||||
|
UsedNames.Add(PinName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UK2Node_ReadSlashCommand::GetTooltipText() const
|
||||||
|
{
|
||||||
|
static FText Tooltip = FText::FromString(TEXT(
|
||||||
|
"Parse a slash command.\n"
|
||||||
|
"\n"
|
||||||
|
"The prototype must be a hardwired string. The first word\n"
|
||||||
|
"is the command name; each remaining word names a value to\n"
|
||||||
|
"read and becomes an output pin.\n"
|
||||||
|
"\n"
|
||||||
|
"For example:\n"
|
||||||
|
"\n"
|
||||||
|
" /command Integer Float\n"
|
||||||
|
"\n"
|
||||||
|
"Supported types: Token, Integer, Float, Rest\n"));
|
||||||
|
return Tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_ReadSlashCommand::ReconstructNode()
|
||||||
|
{
|
||||||
|
// Save the value of the Prototype Pin before it gets reconstructed.
|
||||||
|
UEdGraphPin* PrototypePin = FindPin(PrototypePinName);
|
||||||
|
if (PrototypePin != nullptr)
|
||||||
|
{
|
||||||
|
ValuePrototype = PrototypePin->DefaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Super::ReconstructNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_ReadSlashCommand::AllocateDefaultPins()
|
||||||
|
{
|
||||||
|
Pins.Reset();
|
||||||
|
Super::AllocateDefaultPins();
|
||||||
|
|
||||||
|
TArray<ParsingStep> Steps;
|
||||||
|
ParsePrototype(ValuePrototype, Steps);
|
||||||
|
|
||||||
|
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
|
||||||
|
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, ErrorPinName);
|
||||||
|
|
||||||
|
UEdGraphPin *PrototypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, PrototypePinName);
|
||||||
|
PrototypePin->DefaultValue = ValuePrototype;
|
||||||
|
|
||||||
|
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UlxSlashCommand::StaticClass(), InputValuesPinName);
|
||||||
|
|
||||||
|
// Create an output pin for each value word.
|
||||||
|
for (int32 i = 1; i < Steps.Num(); i++)
|
||||||
|
{
|
||||||
|
// The value comes back through the function's "Result" out-parameter.
|
||||||
|
CreatePin(EGPD_Output, PropertyToPinType(Steps[i].ReadFunction->FindPropertyByName(TEXT("Result"))), Steps[i].PinName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FText UK2Node_ReadSlashCommand::GetNodeTitle(ENodeTitleType::Type TitleType) const
|
||||||
|
{
|
||||||
|
return LOCTEXT("ReadSlashCommand_Title", "Read Slash Command");
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UK2Node_ReadSlashCommand::GetPinDisplayName(const UEdGraphPin* Pin) const
|
||||||
|
{
|
||||||
|
// These pins don't need labels.
|
||||||
|
if ((Pin->PinName == UEdGraphSchema_K2::PN_Execute) ||
|
||||||
|
(Pin->PinName == UEdGraphSchema_K2::PN_Then) ||
|
||||||
|
(Pin->PinName == PrototypePinName))
|
||||||
|
{
|
||||||
|
return FText::GetEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the pin name, removing R: prefix if present.
|
||||||
|
return FText::FromName(RemovePrefix(Pin->PinName));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_ReadSlashCommand::PinDefaultValueChanged(UEdGraphPin* Pin)
|
||||||
|
{
|
||||||
|
if ((Pin->PinName == PrototypePinName) && (Pin->DefaultValue != ValuePrototype))
|
||||||
|
{
|
||||||
|
ReconstructNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void UK2Node_ReadSlashCommand::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
|
||||||
|
{
|
||||||
|
Super::ExpandNode(CompilerContext, SourceGraph);
|
||||||
|
|
||||||
|
TArray<ParsingStep> Steps;
|
||||||
|
if (!ParsePrototype(ValuePrototype, Steps))
|
||||||
|
{
|
||||||
|
CompilerContext.MessageLog.Error(*ErrorMsg);
|
||||||
|
BreakAllNodeLinks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin *InputSlashCommandPin = FindPinChecked(InputValuesPinName);
|
||||||
|
UEdGraphPin *ErrorExecPin = FindPinChecked(ErrorPinName);
|
||||||
|
|
||||||
|
UFunction *CheckCommandFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(TEXT("CheckCommand"));
|
||||||
|
UK2Node_CallFunction *CheckCommandNode = MakeCallFunctionNode(CompilerContext, SourceGraph, CheckCommandFunc);
|
||||||
|
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *CheckCommandNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
|
||||||
|
CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *CheckCommandNode->GetExecPin());
|
||||||
|
CheckCommandNode->FindPinChecked(TEXT("Literal"))->DefaultValue = Steps[0].Word;
|
||||||
|
CheckCommandNode->FindPinChecked(TEXT("Prototype"))->DefaultValue = ValuePrototype;
|
||||||
|
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *CheckCommandNode->FindPinChecked(TEXT("Error")));
|
||||||
|
UEdGraphPin *ThenPin = CheckCommandNode->FindPinChecked(TEXT("Success"));
|
||||||
|
|
||||||
|
for (int32 i = 1; i < Steps.Num(); i++)
|
||||||
|
{
|
||||||
|
UK2Node_CallFunction *ReadNode = MakeCallFunctionNode(CompilerContext, SourceGraph, Steps[i].ReadFunction);
|
||||||
|
CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *ReadNode->FindPinChecked(UEdGraphSchema_K2::PN_Self));
|
||||||
|
CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *ReadNode->FindPinChecked(TEXT("Error")));
|
||||||
|
UEdGraphPin *OutputPin = FindPinChecked(Steps[i].PinName);
|
||||||
|
CompilerContext.MovePinLinksToIntermediate(*OutputPin, *ReadNode->FindPinChecked(TEXT("Result")));
|
||||||
|
ThenPin = ChainExecPin(ThenPin, ReadNode, TEXT("Success"));
|
||||||
|
}
|
||||||
|
|
||||||
|
CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *ThenPin);
|
||||||
|
|
||||||
|
BreakAllNodeLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
UK2Node::ERedirectType UK2Node_ReadSlashCommand::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const
|
||||||
|
{
|
||||||
|
if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None;
|
||||||
|
if ((NewPin->PinName == OldPin->PinName) &&
|
||||||
|
(NewPin->Direction == OldPin->Direction) &&
|
||||||
|
(NewPin->PinType == OldPin->PinType))
|
||||||
|
{
|
||||||
|
return ERedirectType_Name;
|
||||||
|
}
|
||||||
|
return ERedirectType_None;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_ReadSlashCommand::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const
|
||||||
|
{
|
||||||
|
// The prototype pin cannot be connected.
|
||||||
|
if (MyPin->PinName == PrototypePinName)
|
||||||
|
{
|
||||||
|
OutReason = LOCTEXT("Error_PrototypeMustBeHardwired", "Value prototype must be a hardwired constant.").ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_ReadSlashCommand::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
|
||||||
|
{
|
||||||
|
UClass* ActionKey = GetClass();
|
||||||
|
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
|
||||||
|
{
|
||||||
|
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
|
||||||
|
check(NodeSpawner != nullptr);
|
||||||
|
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UK2Node_ReadSlashCommand::GetMenuCategory() const
|
||||||
|
{
|
||||||
|
return FText::FromString(FString(TEXT("Luprex|Slash Commands")));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
74
Source/Integration/ReadSlashCommand.h
Normal file
74
Source/Integration/ReadSlashCommand.h
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// ReadSlashCommand.h
|
||||||
|
//
|
||||||
|
// K2Node that reads typed values from a UlxLuaValues array.
|
||||||
|
// Takes a prototype string like "string x, float y, int z"
|
||||||
|
// and creates output pins with the appropriate types.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "LuprexK2Node.h"
|
||||||
|
|
||||||
|
#include "ReadSlashCommand.generated.h"
|
||||||
|
|
||||||
|
class FBlueprintActionDatabaseRegistrar;
|
||||||
|
class FString;
|
||||||
|
class UEdGraph;
|
||||||
|
class UObject;
|
||||||
|
|
||||||
|
UCLASS(MinimalAPI)
|
||||||
|
class UK2Node_ReadSlashCommand : public UlxK2Node
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
//~ Begin UEdGraphNode Interface.
|
||||||
|
virtual void AllocateDefaultPins() override;
|
||||||
|
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
|
||||||
|
virtual bool ShouldShowNodeProperties() const override { return true; }
|
||||||
|
virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;
|
||||||
|
virtual FText GetTooltipText() const override;
|
||||||
|
virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override;
|
||||||
|
//~ End UEdGraphNode Interface.
|
||||||
|
|
||||||
|
//~ Begin UK2Node Interface.
|
||||||
|
virtual bool IsNodePure() const override { return false; }
|
||||||
|
virtual void ReconstructNode() override;
|
||||||
|
virtual bool NodeCausesStructuralBlueprintChange() const override { return true; }
|
||||||
|
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
|
||||||
|
virtual ERedirectType DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const override;
|
||||||
|
virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override;
|
||||||
|
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
|
||||||
|
virtual FText GetMenuCategory() const override;
|
||||||
|
virtual int32 GetNodeRefreshPriority() const override { return EBaseNodeRefreshPriority::Low_UsesDependentWildcard; }
|
||||||
|
//~ End UK2Node Interface.
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const FName PrototypePinName;
|
||||||
|
static const FName InputValuesPinName;
|
||||||
|
static const FName ErrorPinName;
|
||||||
|
|
||||||
|
struct ParsingStep
|
||||||
|
{
|
||||||
|
FString Word;
|
||||||
|
FName PinName;
|
||||||
|
UFunction *ReadFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ParsePrototype(const FString &Prototype, TArray<ParsingStep>& Steps);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Whenever the prototype pin value changes, we call
|
||||||
|
// ReconstructNode, which backs up the value into this
|
||||||
|
// property. This cache is needed because during
|
||||||
|
// ReconstructNode, we blow away the prototype pin. The
|
||||||
|
// prototype pin is also absent when the node is first
|
||||||
|
// created.
|
||||||
|
//
|
||||||
|
UPROPERTY()
|
||||||
|
FString ValuePrototype = TEXT("/command Integer Float");
|
||||||
|
|
||||||
|
};
|
||||||
157
Source/Integration/SlashCommand.cpp
Normal file
157
Source/Integration/SlashCommand.cpp
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#include "SlashCommand.h"
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Containers/StringView.h"
|
||||||
|
#include "Misc/Char.h"
|
||||||
|
#include "Misc/DefaultValueHelper.h"
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// SlashCommand
|
||||||
|
//
|
||||||
|
// A command line plus a cursor, exposed to blueprint.
|
||||||
|
//
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
UlxSlashCommand* UlxSlashCommand::MakeSlashCommand(const FString& CommandLine)
|
||||||
|
{
|
||||||
|
UlxSlashCommand* Result = NewObject<UlxSlashCommand>();
|
||||||
|
Result->CommandLine = CommandLine;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool UlxSlashCommand::IsSlashCommand(const FString& Command)
|
||||||
|
{
|
||||||
|
if (Command.Len() < 2 || Command[0] != TEXT('/'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every character after the slash must be alphanumeric.
|
||||||
|
for (int32 i = 1; i < Command.Len(); i++)
|
||||||
|
{
|
||||||
|
if (!FChar::IsAlnum(Command[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FStringView UlxSlashCommand::FetchToken()
|
||||||
|
{
|
||||||
|
const TCHAR* Data = *CommandLine;
|
||||||
|
int32 Len = CommandLine.Len();
|
||||||
|
|
||||||
|
// Skip leading whitespace.
|
||||||
|
while (Cursor < Len && FChar::IsWhitespace(Data[Cursor]))
|
||||||
|
{
|
||||||
|
Cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read characters up to the next whitespace.
|
||||||
|
int32 Start = Cursor;
|
||||||
|
while (Cursor < Len && !FChar::IsWhitespace(Data[Cursor]))
|
||||||
|
{
|
||||||
|
Cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FStringView(Data + Start, Cursor - Start);
|
||||||
|
}
|
||||||
|
|
||||||
|
ElxSuccessOrError UlxSlashCommand::CheckCommand(const FString& Literal, const FString& Prototype)
|
||||||
|
{
|
||||||
|
KnownCommands.Add(Literal);
|
||||||
|
|
||||||
|
// Checking the command always starts a fresh parse.
|
||||||
|
Cursor = 0;
|
||||||
|
FStringView Token = FetchToken();
|
||||||
|
if (Token.Equals(Literal, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
MatchingPrototypes.Add(Prototype);
|
||||||
|
return ElxSuccessOrError::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElxSuccessOrError::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElxSuccessOrError UlxSlashCommand::ReadToken(FString& Result)
|
||||||
|
{
|
||||||
|
FStringView Token = FetchToken();
|
||||||
|
if (Token.IsEmpty())
|
||||||
|
{
|
||||||
|
Result.Empty();
|
||||||
|
return ElxSuccessOrError::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = FString(Token);
|
||||||
|
return ElxSuccessOrError::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ElxSuccessOrError UlxSlashCommand::ReadInteger(int32& Result)
|
||||||
|
{
|
||||||
|
// ParseInt validates the whole token and converts it, so "12abc"
|
||||||
|
// is rejected rather than read as a partial number.
|
||||||
|
FStringView Token = FetchToken();
|
||||||
|
if (!FDefaultValueHelper::ParseInt(FString(Token), Result))
|
||||||
|
{
|
||||||
|
Result = 0;
|
||||||
|
return ElxSuccessOrError::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElxSuccessOrError::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ElxSuccessOrError UlxSlashCommand::ReadFloat(double& Result)
|
||||||
|
{
|
||||||
|
// ParseDouble validates the whole token and converts it, so
|
||||||
|
// "12abc" is rejected rather than read as a partial number.
|
||||||
|
FStringView Token = FetchToken();
|
||||||
|
if (!FDefaultValueHelper::ParseDouble(FString(Token), Result))
|
||||||
|
{
|
||||||
|
Result = 0.0;
|
||||||
|
return ElxSuccessOrError::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElxSuccessOrError::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ElxSuccessOrError UlxSlashCommand::ReadRest(FString& Result)
|
||||||
|
{
|
||||||
|
const TCHAR* Data = *CommandLine;
|
||||||
|
int32 Len = CommandLine.Len();
|
||||||
|
|
||||||
|
// Everything from the cursor to the end of the line, trimmed.
|
||||||
|
Result = FString(FStringView(Data + Cursor, Len - Cursor));
|
||||||
|
Result.TrimStartAndEndInline();
|
||||||
|
Cursor = Len;
|
||||||
|
return ElxSuccessOrError::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UlxSlashCommand::GetErrorMessage() const
|
||||||
|
{
|
||||||
|
if (MatchingPrototypes.Num() == 0)
|
||||||
|
{
|
||||||
|
TArray<FString> Commands;
|
||||||
|
for (const FString& Command : KnownCommands)
|
||||||
|
{
|
||||||
|
Commands.Add(Command);
|
||||||
|
}
|
||||||
|
Commands.Sort();
|
||||||
|
return FString(TEXT("No such slash command. Valid slash commands: ")) + FString::Join(Commands, TEXT(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Result = TEXT("Invalid parameters. Valid parameters:\n");
|
||||||
|
for (const FString& Prototype : MatchingPrototypes)
|
||||||
|
{
|
||||||
|
Result += Prototype;
|
||||||
|
Result += TEXT("\n");
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
121
Source/Integration/SlashCommand.h
Normal file
121
Source/Integration/SlashCommand.h
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// SlashCommand.h
|
||||||
|
//
|
||||||
|
// A command line plus a cursor, exposed to blueprint.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Containers/Array.h"
|
||||||
|
#include "Containers/Set.h"
|
||||||
|
#include "Containers/UnrealString.h"
|
||||||
|
#include "Containers/StringFwd.h"
|
||||||
|
#include "Common.h"
|
||||||
|
#include "SlashCommand.generated.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// UlxSlashCommand
|
||||||
|
//
|
||||||
|
// Holds a command line (a string the user has typed) together
|
||||||
|
// with a cursor marking the current position within it. This
|
||||||
|
// is the object that wraps Unreal's FParse facilities so that
|
||||||
|
// blueprint can parse a typed command piece by piece, with the
|
||||||
|
// cursor advancing as each token is consumed.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class UlxSlashCommand : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The full command line that we are parsing.
|
||||||
|
//
|
||||||
|
FString CommandLine;
|
||||||
|
|
||||||
|
// The current parse position: an index into CommandLine.
|
||||||
|
//
|
||||||
|
int32 Cursor = 0;
|
||||||
|
|
||||||
|
// Known command names that have been registered while exploring
|
||||||
|
// possible parses for this line.
|
||||||
|
//
|
||||||
|
TSet<FString> KnownCommands;
|
||||||
|
|
||||||
|
// Prototypes whose command name matches this line.
|
||||||
|
//
|
||||||
|
TArray<FString> MatchingPrototypes;
|
||||||
|
|
||||||
|
// Skip leading whitespace at the cursor, then read characters up
|
||||||
|
// to (but not including) the next whitespace. The token is
|
||||||
|
// returned as a view into CommandLine, and the cursor is advanced
|
||||||
|
// past it.
|
||||||
|
//
|
||||||
|
// Returns an empty view if there is no nonwhitespace input left.
|
||||||
|
//
|
||||||
|
FStringView FetchToken();
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Construct a slash command from a command line string.
|
||||||
|
// The cursor starts at the beginning.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
static UlxSlashCommand* MakeSlashCommand(const FString& CommandLine);
|
||||||
|
|
||||||
|
// Return true if the string is a slash followed by one or more
|
||||||
|
// alphanumeric characters, and nothing else (e.g. "/foo").
|
||||||
|
//
|
||||||
|
static bool IsSlashCommand(const FString& Command);
|
||||||
|
|
||||||
|
// Reset the cursor to the start, then read the first token and
|
||||||
|
// check whether it matches the given literal, case-insensitively.
|
||||||
|
// The command name is recorded in KnownCommands either way. If it
|
||||||
|
// matched, the prototype is added to MatchingPrototypes. The token
|
||||||
|
// is consumed either way. Returns Success if it matched, Error
|
||||||
|
// otherwise.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||||
|
ElxSuccessOrError CheckCommand(const FString& Literal, const FString& Prototype);
|
||||||
|
|
||||||
|
// Read the next whitespace-delimited word from the command line.
|
||||||
|
//
|
||||||
|
// This is the blueprint-callable form of FetchToken: it returns
|
||||||
|
// the token as an FString. Returns Error (with an empty Result)
|
||||||
|
// if there is no nonwhitespace input left, Success otherwise.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||||
|
ElxSuccessOrError ReadToken(FString& Result);
|
||||||
|
|
||||||
|
// Read the next token and interpret it as an integer, using C++
|
||||||
|
// number syntax (optional sign; decimal, 0x hex, or leading-0
|
||||||
|
// octal). The whole token must be valid; "12abc" is rejected.
|
||||||
|
// Returns Error (with Result 0) on failure, Success otherwise.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||||
|
ElxSuccessOrError ReadInteger(int32& Result);
|
||||||
|
|
||||||
|
// Read the next token and interpret it as a floating-point number,
|
||||||
|
// using C++ number syntax (optional sign, digits, dot, e/E
|
||||||
|
// exponent, trailing f). The whole token must be valid; "12abc"
|
||||||
|
// is rejected. Returns Error (with Result 0) on failure, Success
|
||||||
|
// otherwise.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||||
|
ElxSuccessOrError ReadFloat(double& Result);
|
||||||
|
|
||||||
|
// Read the rest of the command line, from the cursor to the end,
|
||||||
|
// trimmed of leading and trailing whitespace. The cursor is
|
||||||
|
// advanced to the end. Always returns Success.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", ExpandEnumAsExecs = "ReturnValue"))
|
||||||
|
ElxSuccessOrError ReadRest(FString& Result);
|
||||||
|
|
||||||
|
// Return a user-facing error message describing why parsing failed.
|
||||||
|
//
|
||||||
|
UFUNCTION(BlueprintCallable)
|
||||||
|
FString GetErrorMessage() const;
|
||||||
|
};
|
||||||
@@ -10,10 +10,13 @@
|
|||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "Blueprint/UserWidget.h"
|
#include "Blueprint/UserWidget.h"
|
||||||
#include "Components/GridPanel.h"
|
#include "Components/GridPanel.h"
|
||||||
|
#include "Components/CanvasPanelSlot.h"
|
||||||
|
#include "Components/Widget.h"
|
||||||
#include "InputMappingContext.h"
|
#include "InputMappingContext.h"
|
||||||
#include "EnhancedInputComponent.h"
|
#include "EnhancedInputComponent.h"
|
||||||
#include "Animation/AnimSequenceBase.h"
|
#include "Animation/AnimSequenceBase.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/InputSettings.h"
|
||||||
|
|
||||||
|
|
||||||
#define LOCTEXT_NAMESPACE "Luprex Utility"
|
#define LOCTEXT_NAMESPACE "Luprex Utility"
|
||||||
@@ -155,65 +158,28 @@ bool UlxUtilityLibrary::LineTraceThroughPixel(const APlayerController* PlayerCon
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UlxUtilityLibrary::SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY)
|
void UlxUtilityLibrary::ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent)
|
||||||
{
|
{
|
||||||
if ((GridPanel == nullptr) || (GridPanel->ColumnFill.Num() != 3) || (GridPanel->RowFill.Num() != 3))
|
UCanvasPanelSlot *CanvasSlot = Cast<UCanvasPanelSlot>(Target);
|
||||||
|
if (CanvasSlot == nullptr)
|
||||||
{
|
{
|
||||||
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
|
UWidget *Widget = Cast<UWidget>(Target);
|
||||||
|
if (Widget != nullptr)
|
||||||
|
{
|
||||||
|
CanvasSlot = Cast<UCanvasPanelSlot>(Widget->Slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CanvasSlot == nullptr)
|
||||||
|
{
|
||||||
|
UE_LOG(LogBlueprint, Error, TEXT("ConfigureCanvasPanelSlot: object is not a CanvasPanelSlot, and is not a Widget in a CanvasPanel."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((LowerRightXY.X < UpperLeftXY.X) || (LowerRightXY.Y < UpperLeftXY.Y))
|
CanvasSlot->SetAnchors(Anchors);
|
||||||
{
|
CanvasSlot->SetAlignment(Alignment);
|
||||||
UE_LOG(LogBlueprint, Error, TEXT("LowerRightXY must be greater than or equal to UpperLeftXY"));
|
CanvasSlot->SetPosition(Position);
|
||||||
return;
|
CanvasSlot->SetSize(Size);
|
||||||
}
|
CanvasSlot->SetAutoSize(SizeToContent);
|
||||||
|
|
||||||
UpperLeftXY.X = FMath::Clamp(UpperLeftXY.X, 0.0f, 1.0f);
|
|
||||||
UpperLeftXY.Y = FMath::Clamp(UpperLeftXY.Y, 0.0f, 1.0f);
|
|
||||||
LowerRightXY.X = FMath::Clamp(LowerRightXY.X, 0.0f, 1.0f);
|
|
||||||
LowerRightXY.Y = FMath::Clamp(LowerRightXY.Y, 0.0f, 1.0f);
|
|
||||||
|
|
||||||
GridPanel->SetRowFill(0, UpperLeftXY.Y);
|
|
||||||
GridPanel->SetRowFill(1, LowerRightXY.Y - UpperLeftXY.Y);
|
|
||||||
GridPanel->SetRowFill(2, 1.0 - LowerRightXY.Y);
|
|
||||||
|
|
||||||
GridPanel->SetColumnFill(0, UpperLeftXY.X);
|
|
||||||
GridPanel->SetColumnFill(1, LowerRightXY.X - UpperLeftXY.X);
|
|
||||||
GridPanel->SetColumnFill(2, 1.0 - LowerRightXY.X);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UlxUtilityLibrary::GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY)
|
|
||||||
{
|
|
||||||
TArray<float> &Col = GridPanel->ColumnFill;
|
|
||||||
TArray<float> &Row = GridPanel->RowFill;
|
|
||||||
|
|
||||||
// Set default return value for error situations.
|
|
||||||
UpperLeftXY.X = 0.0;
|
|
||||||
LowerRightXY.X = 1.0;
|
|
||||||
UpperLeftXY.Y = 0.0;
|
|
||||||
LowerRightXY.Y = 1.0;
|
|
||||||
|
|
||||||
if ((GridPanel == nullptr) || (Row.Num() != 3) || (Col.Num() != 3))
|
|
||||||
{
|
|
||||||
UE_LOG(LogBlueprint, Error, TEXT("SetPositionOfGridPanelMiddleCell only works on 3x3 GridPanels."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double TotalX = Col[0] + Col[1] + Col[2];
|
|
||||||
double TotalY = Row[0] + Row[1] + Row[2];
|
|
||||||
|
|
||||||
if (TotalX > 0)
|
|
||||||
{
|
|
||||||
UpperLeftXY.X = Col[0] / TotalX;
|
|
||||||
LowerRightXY.X = (Col[0] + Col[1]) / TotalX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TotalY > 0)
|
|
||||||
{
|
|
||||||
UpperLeftXY.Y = Row[0] / TotalY;
|
|
||||||
LowerRightXY.Y = (Row[0] + Row[1]) / TotalY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ElxUsedOrNotUsed UlxUtilityLibrary::IsKeyUsedByMappingContext(const FKey &Key, const UInputMappingContext *MappingContext)
|
ElxUsedOrNotUsed UlxUtilityLibrary::IsKeyUsedByMappingContext(const FKey &Key, const UInputMappingContext *MappingContext)
|
||||||
@@ -275,3 +241,4 @@ void UlxUtilityLibrary::ValidateLuaExpr(
|
|||||||
FlxLockedWrapper w;
|
FlxLockedWrapper w;
|
||||||
Status = w.ValidateLuaExpr(Code, ErrorMessage);
|
Status = w.ValidateLuaExpr(Code, ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "Input/Events.h"
|
#include "Input/Events.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "Components/CanvasPanelSlot.h"
|
||||||
|
|
||||||
#include "UtilityLibrary.generated.h"
|
#include "UtilityLibrary.generated.h"
|
||||||
|
|
||||||
@@ -91,51 +92,15 @@ public:
|
|||||||
ETraceTypeQuery TraceChannel, bool bTraceComplex, EDrawDebugTrace::Type DrawDebugType, bool bIgnorePlayerPawn,
|
ETraceTypeQuery TraceChannel, bool bTraceComplex, EDrawDebugTrace::Type DrawDebugType, bool bIgnorePlayerPawn,
|
||||||
const TArray<AActor*>& ActorsToIgnore, FHitResult& HitResult);
|
const TArray<AActor*>& ActorsToIgnore, FHitResult& HitResult);
|
||||||
|
|
||||||
// Set Position of GridPanel Middle Cell
|
|
||||||
//
|
|
||||||
// Sometimes, you want to specify the position of a widget, and you
|
|
||||||
// don't want to specify the position in slate units, instead, you
|
|
||||||
// want to specify the position using fractions: ie, (0,0) is the
|
|
||||||
// upper left corner of the screen, and (1,1) is the lower-right corner.
|
|
||||||
//
|
|
||||||
// One way to accomplish this is to put your widget in the middle cell
|
|
||||||
// of a 3x3 GridPanel. Then, you can position it by adjusting the grid
|
|
||||||
// fill rules. This utility routine can do the math necessary to
|
|
||||||
// correctly populate those fill rules.
|
|
||||||
//
|
|
||||||
// This routine must be passed a 3x3 GridPanel. This will reposition
|
|
||||||
// the middle cell. You must specify the upper-left and lower-right
|
|
||||||
// corners of the middle cell as fractions between (0,0) and (1,1).
|
|
||||||
//
|
|
||||||
// Be aware that if the content of a grid cell overflows the amount of
|
|
||||||
// space allocated for it, then the grid will adjust to make room.
|
|
||||||
// But that will mean that the grid is no longer faithful to the
|
|
||||||
// positions specified in its fill rules. One way to ensure that the
|
|
||||||
// grid remains faithful to its fill rules is to put an Overlay
|
|
||||||
// into the GridPanel cell, then put -1000 padding into the
|
|
||||||
// Overlay's GridPanel slot, then put +1000 padding into the Overlay
|
|
||||||
// slot. The two paddings cancel each other out, leaving the item in
|
|
||||||
// the Overlay at the originally-intended position. But if the item
|
|
||||||
// in the overlay overflows, it doesn't cause the Grid to deform.
|
|
||||||
// Instead, the item exceeds the bounds of the grid cell, but it leaves
|
|
||||||
// the grid cell where it belongs.
|
|
||||||
//
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Widget")
|
|
||||||
static void SetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D UpperLeftXY, FVector2D LowerRightXY);
|
|
||||||
|
|
||||||
// Get Position of GridPanel Middle Cell
|
// Configure a CanvasPanelSlot's parameters in a single call.
|
||||||
//
|
//
|
||||||
// The routine must be passed a 3x3 GridPanel. This will return the
|
// Target must be either a UCanvasPanelSlot directly, or a UWidget whose
|
||||||
// position of the middle cell of the gridpanel, expressed on a scale
|
// Slot is a UCanvasPanelSlot. If it is neither, logs an error and
|
||||||
// from (0,0) to (1,1).
|
// does nothing.
|
||||||
//
|
|
||||||
// The numbers returned by this routine are based entirely on the
|
|
||||||
// GridPanel fill rules. If an item in the grid is overflowing its
|
|
||||||
// allocated space, causing the grid to deform, then that won't be
|
|
||||||
// reflected in the output of this routine.
|
|
||||||
//
|
//
|
||||||
UFUNCTION(BlueprintPure, Category="Widget")
|
UFUNCTION(BlueprintCallable, Category = "Widget", meta = (SizeToContent = "true"))
|
||||||
static void GetPositionOfGridPanelMiddleCell(UGridPanel *GridPanel, FVector2D &UpperLeftXY, FVector2D &LowerRightXY);
|
static void ConfigureCanvasPanelSlot(UObject *Target, FAnchors Anchors, FVector2D Position, FVector2D Size, FVector2D Alignment, bool SizeToContent);
|
||||||
|
|
||||||
// Check if a given key is used by the specified mapping context.
|
// Check if a given key is used by the specified mapping context.
|
||||||
//
|
//
|
||||||
|
|||||||
13
build.py
13
build.py
@@ -221,7 +221,10 @@ def unzip_unreal_engine_and_apply_patch():
|
|||||||
print(f"Unzipping {zipfn}...")
|
print(f"Unzipping {zipfn}...")
|
||||||
with JZipFile(zipfn) as zf:
|
with JZipFile(zipfn) as zf:
|
||||||
zf.extractall(INTEGRATION)
|
zf.extractall(INTEGRATION)
|
||||||
patchfile = f"{INTEGRATION}/EnginePatches/EnginePatch"
|
version = zips[0].stem.split("-")[1]
|
||||||
|
patchfile = f"{INTEGRATION}/EnginePatches/EnginePatch-{version}"
|
||||||
|
if not Path(patchfile).is_file():
|
||||||
|
sys.exit(f"Cannot find engine patch: {patchfile}")
|
||||||
shell(extracted, f"patch -p0 < {patchfile}")
|
shell(extracted, f"patch -p0 < {patchfile}")
|
||||||
Path(extracted).rename(UNREALENGINE)
|
Path(extracted).rename(UNREALENGINE)
|
||||||
Path(touchfile).touch()
|
Path(touchfile).touch()
|
||||||
@@ -271,7 +274,7 @@ def run_unrealengine_setup_bat_replacement():
|
|||||||
shell(UNREALENGINE, "Engine/Binaries/DotNET/GitDependencies/win-x64/GitDependencies.exe")
|
shell(UNREALENGINE, "Engine/Binaries/DotNET/GitDependencies/win-x64/GitDependencies.exe")
|
||||||
shell(UNREALENGINE, "Engine/Extras/Redist/en-us/UEPrereqSetup_x64.exe /quiet /norestart")
|
shell(UNREALENGINE, "Engine/Extras/Redist/en-us/UEPrereqSetup_x64.exe /quiet /norestart")
|
||||||
else:
|
else:
|
||||||
shell(UNREALENGINE, "Engine/Build/BatchFiles/Linux/GitDependencies.sh")
|
shell(UNREALENGINE, f"Engine/Build/BatchFiles/Linux/GitDependencies.sh --cache={INTEGRATION}/.gitdeps-cache")
|
||||||
shell(f"{UNREALENGINE}/Engine/Build/BatchFiles/Linux", "./Setup.sh")
|
shell(f"{UNREALENGINE}/Engine/Build/BatchFiles/Linux", "./Setup.sh")
|
||||||
touch.write_text("Downloaded")
|
touch.write_text("Downloaded")
|
||||||
|
|
||||||
@@ -309,7 +312,7 @@ def build_compile_commands_from_luprex():
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
if (parts[0] == "g++") and ("-c" in parts):
|
if (parts[0] == "g++") and ("-c" in parts):
|
||||||
source = os.path.abspath(os.path.join(INTEGRATION, parts[-1]))
|
source = os.path.abspath(os.path.join(INTEGRATION, parts[-1]))
|
||||||
entries.append({ "file": source, "command": line.replace("g++ ", "g++ -D__INTELLISENSE__ ", 1), "directory": INTEGRATION })
|
entries.append({ "file": source, "command": line, "directory": INTEGRATION })
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@@ -349,7 +352,7 @@ def build_compile_commands_from_integration():
|
|||||||
ccdir = f"{UNREALENGINE}/Engine/Source"
|
ccdir = f"{UNREALENGINE}/Engine/Source"
|
||||||
for cpp in sorted(cpp_to_rsp.keys()):
|
for cpp in sorted(cpp_to_rsp.keys()):
|
||||||
rsp = cpp_to_rsp[cpp]
|
rsp = cpp_to_rsp[cpp]
|
||||||
args = [clang, "-D__INTELLISENSE__", "@"+rsp]
|
args = [clang, "@"+rsp]
|
||||||
entries.append({ "file": cpp, "arguments": args, "directory": ccdir })
|
entries.append({ "file": cpp, "arguments": args, "directory": ccdir })
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
@@ -408,7 +411,7 @@ def build_clean():
|
|||||||
which resets your git repository to its pristine state.
|
which resets your git repository to its pristine state.
|
||||||
DANGER: this deletes any new source code you've created!
|
DANGER: this deletes any new source code you've created!
|
||||||
"""
|
"""
|
||||||
shell(f"{INTEGRATION}/luprex", "make clean")
|
shell(f"{INTEGRATION}", "make -f luprex/Makefile clean")
|
||||||
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} {DEBUG} {INTEGRATION}/Integration.uproject -clean")
|
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} {DEBUG} {INTEGRATION}/Integration.uproject -clean")
|
||||||
Path(f"{INTEGRATION}/.vscode/compile_commands.json").unlink(missing_ok = True)
|
Path(f"{INTEGRATION}/.vscode/compile_commands.json").unlink(missing_ok = True)
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ eng::string LuaCoreStack::load(LuaSlot result, std::string_view code, std::strin
|
|||||||
const char *str = lua_tolstring(L_, -1, &len);
|
const char *str = lua_tolstring(L_, -1, &len);
|
||||||
eng::string message(str, len);
|
eng::string message(str, len);
|
||||||
lua_pop(L_, 1);
|
lua_pop(L_, 1);
|
||||||
if (sv::has_suffix(message, "near <eof>"))
|
if (sv::has_suffix(message, "near <eof>") && sv::is_possible_long_lua_expression(code))
|
||||||
{
|
{
|
||||||
message = "truncated lua";
|
message = "truncated lua";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ bool PrintChanneler::channel(const PrintBuffer *printbuffer, StreamBuffer *sb) {
|
|||||||
line_ = printbuffer->first_line();
|
line_ = printbuffer->first_line();
|
||||||
}
|
}
|
||||||
while (line_ < printbuffer->first_unchecked()) {
|
while (line_ < printbuffer->first_unchecked()) {
|
||||||
sb->write_bytes("|");
|
|
||||||
sb->write_bytes(printbuffer->nth(line_));
|
sb->write_bytes(printbuffer->nth(line_));
|
||||||
sb->write_bytes("\n");
|
sb->write_bytes("\n");
|
||||||
line_ += 1;
|
line_ += 1;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user