Compare commits
68 Commits
85a6fad139
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d737879ed6 | |||
| 46051526e6 | |||
| 8dab0d16b7 | |||
| 933c1ac6c3 | |||
| 521d4726ad | |||
| 2bfa3024f1 | |||
| f7983b1f02 | |||
| 36ec4a3b9b | |||
| 1c2be1b4d8 | |||
| 0b23c82e73 | |||
| b1defd821b | |||
| e17f5417f2 | |||
| c0848c2670 | |||
| 94e6385f14 | |||
| 1328f6e5f7 | |||
| 5d2377df1d | |||
| e0d45cc1db | |||
| ff9c045c8e | |||
| e669140e2c | |||
| 420ea088d7 | |||
| b00ec49e91 | |||
| 1aa888ac82 | |||
| 236693fca6 | |||
| b5e121f884 | |||
| ac4302141c | |||
| e16e0978b0 | |||
| 3be98f3617 | |||
| 3cf984ff65 | |||
| 78c85660c9 | |||
| 3e7e6a2ae4 | |||
| 9d37f02d44 | |||
| 97b5a3c593 | |||
| ae0defbad9 | |||
| 9598004e6d | |||
| 4680a0f3f4 | |||
| 3f6ef4b56c | |||
| 960abba07f | |||
| a689d59ea0 | |||
| a964211cc8 | |||
| d985a6bc55 | |||
| 4420c52b74 | |||
| 8e5d43fd24 | |||
| ec983951fe | |||
| 9787522ef6 | |||
| 0d607ba277 | |||
| 4c1eebab96 | |||
| f3e1daf4fe | |||
| 21d8c40005 | |||
| 275698c5aa | |||
| dabb5b8f0b | |||
| fd970f20c3 | |||
| 6388de9b39 | |||
| 7a09da8a4e | |||
| d396f394ab | |||
| f19e8ccb72 | |||
| 6b057d1514 | |||
| 392faff205 | |||
| ae1ad7640d | |||
| 26399a6a15 | |||
| 8a3d200247 | |||
| 2f83910897 | |||
| 7296847fb5 | |||
| 49331773bf | |||
| 07b90ced1a | |||
| 34011e43d5 | |||
| 1db2705877 | |||
| 7a48a54ae5 | |||
| f3d9a903d2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,6 +27,7 @@ UnrealEngine
|
|||||||
*.vcproj
|
*.vcproj
|
||||||
.ignore
|
.ignore
|
||||||
|
|
||||||
|
.gitdeps-cache/**
|
||||||
.vscode/**
|
.vscode/**
|
||||||
Config/**
|
Config/**
|
||||||
Saved/**
|
Saved/**
|
||||||
@@ -52,3 +53,4 @@ GPF-output/**
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.clangd-query/
|
.clangd-query/
|
||||||
COMMIT.txt
|
COMMIT.txt
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"ue-wingman": {
|
|
||||||
"command": "python3",
|
|
||||||
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,9 +25,16 @@
|
|||||||
- `Docs/` — Documentation.
|
- `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 |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
[/Script/Engine.Engine]
|
[/Script/Engine.Engine]
|
||||||
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient
|
GameViewportClientClassName=/Script/Integration.lxViewportClient
|
||||||
|
|
||||||
[/Script/EngineSettings.GameMapsSettings]
|
[/Script/EngineSettings.GameMapsSettings]
|
||||||
GameDefaultMap=/Game/LpxLevel.LpxLevel
|
GameDefaultMap=/Game/LpxLevel.LpxLevel
|
||||||
|
|||||||
@@ -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_AnyHotkey.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_AnyHotkey.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_Console.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Console.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadD.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadD.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadR.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_DPadU.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_DPadU.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceM.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceM.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_FaceR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_FaceR.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_Menu.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_ShoulderL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_ShoulderL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_ShoulderR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_ShoulderR.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_TriggerL.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_TriggerL.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Luprex/InputActions/IA_TriggerR.uasset
LFS
Normal file
BIN
Content/Luprex/InputActions/IA_TriggerR.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
Content/Luprex/lxGameMode.uasset
LFS
BIN
Content/Luprex/lxGameMode.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
BIN
Content/Luprex/lxPrompts.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Testing/BP_Test.uasset
LFS
BIN
Content/Testing/BP_Test.uasset
LFS
Binary file not shown.
BIN
Content/Testing/WB_Test.uasset
LFS
BIN
Content/Testing/WB_Test.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Widgets/WB_Menu.uasset
LFS
Normal file
BIN
Content/Widgets/WB_Menu.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
BIN
Content/Widgets/basic-border.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
BIN
Content/Widgets/teardrop.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
BIN
Content/Widgets/white-dot.uasset
LFS
Normal file
Binary file not shown.
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
# Ideas for BlueprintMCP handler registration
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class USpawnNodeHandler : public UMCPHandler
|
|
||||||
{
|
|
||||||
UPROPERTY()
|
|
||||||
FString BlueprintName;
|
|
||||||
|
|
||||||
UPROPERTY();
|
|
||||||
FString GraphName;
|
|
||||||
|
|
||||||
UPROPERTY();
|
|
||||||
FString ActionName;
|
|
||||||
|
|
||||||
UPROPERTY(meta = "optional");
|
|
||||||
int PosX;
|
|
||||||
|
|
||||||
UPROPERTY(meta = "optional");
|
|
||||||
int PosY;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
ElxLuaValueType ValueType;
|
|
||||||
|
|
||||||
// Dummy field. Nothing is actually extracted by the argument parser,
|
|
||||||
// but this tells the argument parser that the parameter "subtree" is
|
|
||||||
// supposed to be there.
|
|
||||||
UPROPERTY()
|
|
||||||
void Subtree;
|
|
||||||
|
|
||||||
virtual void Handle(Json, Result) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Blueprint Text Export
|
|
||||||
|
|
||||||
Blueprints are stored as binary `.uasset` files that Claude Code cannot read directly. To work around this, a text exporter automatically converts blueprint graphs to readable text files whenever a blueprint is saved in the Unreal editor.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
The game module (`FlxIntegrationModuleImpl` in `Source/Integration/Integration.cpp`) hooks into `UPackage::PackageSavedWithContextEvent`. When a blueprint is saved, it iterates each `UEdGraph` in the blueprint and runs `FlxBlueprintExporter` on it. The output is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`.
|
|
||||||
|
|
||||||
The exporter class (`Source/Integration/BlueprintExporter.h/.cpp`) processes one graph at a time. The constructor runs all passes and the result is available via `GetOutput()`.
|
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
The graph file is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`. A details file with node-name-to-GUID mappings is written to `Saved/BlueprintExports/<BlueprintName>/DETAILS/<GraphName>.txt`.
|
|
||||||
|
|
||||||
Every line in the graph file starts with a keyword, making it easy to parse. The format is:
|
|
||||||
|
|
||||||
```
|
|
||||||
node Event_Tick
|
|
||||||
return Output_Delegate, Delta_Seconds
|
|
||||||
goto Set_Tick_Delta_Seconds
|
|
||||||
|
|
||||||
node Set_Tick_Delta_Seconds
|
|
||||||
input Real Tick_Delta_Seconds = Event_Tick.Delta_Seconds
|
|
||||||
return Output_Get
|
|
||||||
goto CallFunctionByName
|
|
||||||
```
|
|
||||||
|
|
||||||
### Line Keywords
|
|
||||||
|
|
||||||
- `node Name` — starts a new node block.
|
|
||||||
- `input Type Name = Source` — an input data pin. Source is a `Node.Pin` reference, a literal value, `<self>`, or `<default>`.
|
|
||||||
- `return Pin1, Pin2` — output data pins.
|
|
||||||
- `goto Target` — exec flow (single output).
|
|
||||||
- `goto-if PinName Target` — exec flow (multiple outputs, e.g. branch true/false).
|
|
||||||
- `// comment text` — comment node.
|
|
||||||
|
|
||||||
### Special Handling
|
|
||||||
|
|
||||||
- String defaults are shown in quotes.
|
|
||||||
- Variable get nodes are inlined (the variable name appears directly at the point of use rather than as a separate node).
|
|
||||||
- Knot (reroute) nodes are followed through transparently.
|
|
||||||
|
|
||||||
## Node Ordering
|
|
||||||
|
|
||||||
Nodes are sorted by a traversal algorithm: find starter nodes (exec output but no exec input), sort them by Y position, then traverse each. The traversal visits a node's inputs first (so data sources appear before their consumers), then emits the node itself, then follows exec outputs. This produces a readable top-down flow. Any unvisited nodes are appended at the end.
|
|
||||||
|
|
||||||
## Node Naming
|
|
||||||
|
|
||||||
Names are derived from `ENodeTitleType::ListView` titles, sanitized to alphanumeric plus underscores. Duplicates get `_2`, `_3`, etc.
|
|
||||||
117
Docs/Getting-Gamepad-USB-Device-Name.md
Normal file
117
Docs/Getting-Gamepad-USB-Device-Name.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Getting the Gamepad USB Device Name
|
||||||
|
|
||||||
|
Unreal exposes `UInputDeviceSubsystem::GetMostRecentlyUsedHardwareDevice()`, which
|
||||||
|
returns an `FHardwareDeviceIdentifier`. In stock Unreal this is nearly useless: on
|
||||||
|
Windows it toggles between `"WindowsApplication"` (KBM) and `"XInputController"`
|
||||||
|
(any XInput pad); on Linux it returns nothing meaningful at all. It cannot
|
||||||
|
distinguish a DualSense from an Xbox pad.
|
||||||
|
|
||||||
|
The USB-reported device name (e.g. `"Sony Interactive Entertainment Wireless
|
||||||
|
Controller"`) is what we want to surface. This document describes the changes
|
||||||
|
required on each platform to put that name into `FHardwareDeviceIdentifier`.
|
||||||
|
|
||||||
|
## How `FHardwareDeviceIdentifier` Gets Populated
|
||||||
|
|
||||||
|
Input drivers wrap their `MessageHandler->OnControllerButton/Analog` calls in an
|
||||||
|
`FInputDeviceScope` (stack-allocated, thread-local stack of pointers). The scope
|
||||||
|
carries `InputDeviceName` (driver class) and `HardwareDeviceIdentifier` (physical
|
||||||
|
device string). When the input event reaches `UInputDeviceSubsystem`, those two
|
||||||
|
fields are copied into the `FHardwareDeviceIdentifier` record for that user.
|
||||||
|
|
||||||
|
So the fix on each platform is the same shape: **at device-connect time, cache
|
||||||
|
the USB device-name string; in the per-event scope, pass it as the
|
||||||
|
`HardwareDeviceIdentifier`.**
|
||||||
|
|
||||||
|
## Windows: Patch the GameInput Plugin
|
||||||
|
|
||||||
|
Location: `Engine/Plugins/Runtime/GameInput/Source/GameInputBase/`.
|
||||||
|
|
||||||
|
The Microsoft GameInput SDK already exposes the data we need on every device via
|
||||||
|
`GameInputDeviceInfo::displayName` (a `wchar_t*`). The plugin reads it for log
|
||||||
|
lines (`GameInputUtils.cpp:18-23`) but does not propagate it.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. **Cache at connect.** Add `FString CachedDisplayName` to
|
||||||
|
`FGameInputDeviceContainer`. In its constructor / device-info init path
|
||||||
|
(`GameInputDeviceContainer.cpp:44`, where `Info` is in scope), set:
|
||||||
|
```cpp
|
||||||
|
CachedDisplayName = FString(Info->displayName);
|
||||||
|
```
|
||||||
|
`TCHAR == wchar_t` on Windows, so the conversion is direct. Lifetime matches
|
||||||
|
the container — cleared automatically on disconnect.
|
||||||
|
|
||||||
|
2. **Make the container reachable from `FGameInputEventParams`.** The processor
|
||||||
|
already gets a `Device` pointer; add a `Container` pointer so
|
||||||
|
`GetHardwareDeviceIdentifierName` can read the cached string.
|
||||||
|
|
||||||
|
3. **Return it from `GetHardwareDeviceIdentifierName`**
|
||||||
|
(`GameInputDeviceProcessor.cpp:61`). After the existing
|
||||||
|
`bOverrideHardwareDeviceIdString` block, before the family-bucket switch:
|
||||||
|
```cpp
|
||||||
|
if (Params.Container && !Params.Container->CachedDisplayName.IsEmpty())
|
||||||
|
{
|
||||||
|
return Params.Container->CachedDisplayName;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The override path still wins (explicit dev intent); the family-bucket
|
||||||
|
fallback handles devices with empty `displayName`.
|
||||||
|
|
||||||
|
Total: ~10 lines plus the field. All inside `#if GAME_INPUT_SUPPORT`.
|
||||||
|
|
||||||
|
The user must enable the GameInput plugin and disable the default `XInputDevice`
|
||||||
|
plugin (otherwise both drivers will dispatch events for the same pad).
|
||||||
|
|
||||||
|
## Linux: Patch `LinuxApplication.cpp`
|
||||||
|
|
||||||
|
Location: `Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp`.
|
||||||
|
|
||||||
|
SDL already provides the device name via `SDL_GameControllerName()`. The Linux
|
||||||
|
path calls it once for a log line in `AddGameController` (line 2175) and then
|
||||||
|
discards it. The Linux path also does not push an `FInputDeviceScope` at all
|
||||||
|
around its controller events — that's the second half of why
|
||||||
|
`GetMostRecentlyUsedHardwareDevice` is useless on Linux.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. **Store the name.** Add `FString DeviceName` to `SDLControllerState`. In
|
||||||
|
`AddGameController` (~line 2175), assign:
|
||||||
|
```cpp
|
||||||
|
ControllerState.DeviceName = UTF8_TO_TCHAR(SDL_GameControllerName(Controller));
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Wrap controller-event dispatch in a scope.** Around the
|
||||||
|
`MessageHandler->OnControllerButton*/OnControllerAnalog` block (starting
|
||||||
|
~line 518):
|
||||||
|
```cpp
|
||||||
|
FInputDeviceScope Scope(
|
||||||
|
nullptr,
|
||||||
|
FName("LinuxApplication"),
|
||||||
|
ControllerState.DeviceId.GetId(),
|
||||||
|
ControllerState.DeviceName);
|
||||||
|
// ... existing dispatch ...
|
||||||
|
```
|
||||||
|
Use `"LinuxApplication"` for `InputDeviceName` to mirror the Windows
|
||||||
|
convention.
|
||||||
|
|
||||||
|
Total: ~12 lines. No third-party dependencies, no engine plugins, no per-device
|
||||||
|
config tables.
|
||||||
|
|
||||||
|
## What This Does Not Solve
|
||||||
|
|
||||||
|
- **Stock XInput on Windows.** If we keep the default `XInputDevice` plugin,
|
||||||
|
events continue to come through `XInputInterface.cpp` with a fixed
|
||||||
|
`"XInputController"` identifier. XInput itself does not expose VID/PID or
|
||||||
|
device strings. Recovering the name there requires correlating the XInput
|
||||||
|
slot with a Raw Input HID via the `IG_xx` token in the Raw Input device path,
|
||||||
|
then calling `HidD_GetProductString` — a separate, more involved patch.
|
||||||
|
GameInput is the cleaner answer on Windows.
|
||||||
|
|
||||||
|
- **XInput-wrapped pads under GameInput.** A DualSense routed through Steam
|
||||||
|
Input or DS4Windows will surface as the wrapper's display name (e.g. ViGEm),
|
||||||
|
not as the underlying physical device. This is a property of the wrapper, not
|
||||||
|
fixable from our side.
|
||||||
|
|
||||||
|
- **`displayName` is not a stable identifier.** It is a human-readable name
|
||||||
|
whose exact text varies by OS version, driver, and USB descriptor. Use it for
|
||||||
|
prompt-set selection and logging. Do not use it as a save-game key.
|
||||||
311
Docs/Luprex-Window-Management.md
Normal file
311
Docs/Luprex-Window-Management.md
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Unreal has several input mode-related subsystems that
|
||||||
|
interact with each other in complicated ways. These
|
||||||
|
subsystems include:
|
||||||
|
|
||||||
|
- keyboard focus
|
||||||
|
- mouse capture
|
||||||
|
- enhanced input routing
|
||||||
|
- pointer visibility
|
||||||
|
- window z-order
|
||||||
|
|
||||||
|
Unreal is littered with conditionals that cause these bits
|
||||||
|
of state to affect each other in unpredictable, often
|
||||||
|
illogical ways. If you set these bits of state in the wrong
|
||||||
|
order, or to the wrong values, it is all too easy to get
|
||||||
|
unreal into a non-functioning state. The system is *much*
|
||||||
|
too fragile.
|
||||||
|
|
||||||
|
For this reason, I have implemented a window management
|
||||||
|
system that orchestrates all of this from a centralized
|
||||||
|
location, in a way that guarantees reasonably predictable,
|
||||||
|
sane behavior.
|
||||||
|
|
||||||
|
# Core Design Choices
|
||||||
|
|
||||||
|
Our window management system, in order to keep things
|
||||||
|
simple, has to make some assumptions about how Luprex games
|
||||||
|
work. So, here are the rules.
|
||||||
|
|
||||||
|
Top-level UserWidgets get inserted into a "Root Canvas",
|
||||||
|
instead of into the viewport. The root canvas implements most
|
||||||
|
of the functionality of our window management system.
|
||||||
|
|
||||||
|
The keyboard focus rule is simple: the UserWidget in front
|
||||||
|
according to the z-order gets keyboard focus. The window
|
||||||
|
management system will put focus on the front widget and
|
||||||
|
will keep it there. The *only* way to give a UserWidget
|
||||||
|
keyboard focus is to raise it to the front of the z-order.
|
||||||
|
|
||||||
|
Mouse movements events are handled in two different ways:
|
||||||
|
the system can shift between "mouselook" mode and
|
||||||
|
"point-and-click" mode. Every top-level UserWidget declares
|
||||||
|
whether it wants a mouse pointer or not. If the front
|
||||||
|
UserWidget wants a pointer, the system shifts into
|
||||||
|
point-and-click mode.
|
||||||
|
|
||||||
|
In point-and-click mode, enhanced input mouse move events
|
||||||
|
cannot happen. In mouselook mode, widget OnMouseDown and
|
||||||
|
OnMouseMove events cannot happen. In both modes, you can
|
||||||
|
track mouse movement, but you have to use different
|
||||||
|
mechanisms.
|
||||||
|
|
||||||
|
Widgets that declare that they want a pointer are
|
||||||
|
automatically put in front of widgets that don't want a
|
||||||
|
pointer. Because of this rule, the system essentially
|
||||||
|
separates into the "mouselook" layer underneath, and the
|
||||||
|
"point-and-click" layer on top. When the point-and-click
|
||||||
|
layer gets out of the way, then you can drive the 3D world.
|
||||||
|
|
||||||
|
# State Variables of the Window Management System
|
||||||
|
|
||||||
|
I have made an effort to keep the number of state variables
|
||||||
|
that you have to control to an absolute minimum, and to
|
||||||
|
concentrate them all in one place. That place is the "Root
|
||||||
|
Canvas Slot."
|
||||||
|
|
||||||
|
Typically, in Unreal, when you create a new top-level
|
||||||
|
widget, you insert it into the viewport using AddToViewport.
|
||||||
|
But to use our window management system, you must instead
|
||||||
|
insert top-level widgets into a 'root canvas', using
|
||||||
|
AddWidgetToRoot.
|
||||||
|
|
||||||
|
The root canvas object associates a RootCanvasSlot to each
|
||||||
|
top-level widget. The RootCanvasSlot is a place where we can
|
||||||
|
store window management-related hints for that widget. The
|
||||||
|
contents of the RootCanvasSlot include the following:
|
||||||
|
|
||||||
|
- `ShowPointer`: If true, this is a point-and-click widget.
|
||||||
|
When this widget is in front, the pointer is visible,
|
||||||
|
and the system switches to point-and-click mode.
|
||||||
|
|
||||||
|
- `BlockInput`: If this window is in front, all enhanced
|
||||||
|
input events in *other* objects are blocked.
|
||||||
|
|
||||||
|
- `EnableEnhancedInput`: If false, enhanced input events in
|
||||||
|
*this* widget are disabled.
|
||||||
|
|
||||||
|
- `BringToFrontCount`: Effectively, a timestamp indicating
|
||||||
|
the last time this window was brought to the front. This
|
||||||
|
is the main factor determining the z-order of the widgets.
|
||||||
|
|
||||||
|
In addition, the top-level widget itself contains some
|
||||||
|
window-management related properties. Currently, these are:
|
||||||
|
|
||||||
|
- `DesiredFocusWidget`: Indicates which sub-widget, if any,
|
||||||
|
should be given focus. When the system grants focus to
|
||||||
|
the frontmost UserWidget, the focus actually goes here.
|
||||||
|
|
||||||
|
There are deliberately *no other variables* that control our
|
||||||
|
new window management system. If your blueprint is managing
|
||||||
|
these properties, then it is doing everything it needs to
|
||||||
|
do.
|
||||||
|
|
||||||
|
The function SetWidgetWindowManagement can set all of these
|
||||||
|
variables in a single operation. That one function is all
|
||||||
|
you need to control the entire window management system.
|
||||||
|
|
||||||
|
# Handling Keyboard and Gamepad Buttons
|
||||||
|
|
||||||
|
Here is a summary of how keyboard/gamepad button handling in
|
||||||
|
unreal works. We have tweaked this slightly, but this is
|
||||||
|
mostly just ordinary unreal input handling:
|
||||||
|
|
||||||
|
When you press a keyboard or gamepad button, the button
|
||||||
|
first goes to any widget that has keyboard focus. If that
|
||||||
|
widget doesn't declare the button to be "handled", then
|
||||||
|
button is offered to other widgets higher in the widget
|
||||||
|
heirarchy. If no widget handles the button, the button then
|
||||||
|
goes to the "enhanced input subsystem."
|
||||||
|
|
||||||
|
The enhanced input system puts the button through
|
||||||
|
an "input mapping context." Basically, that's a many-to-one
|
||||||
|
map that translates buttons into more abstract "enhanced
|
||||||
|
input events." Here's a fragment of a typical input
|
||||||
|
mapping context:
|
||||||
|
|
||||||
|
Key W --> IA_Move_Forward
|
||||||
|
Key S --> IA_Move_Backward
|
||||||
|
Left_Thumbstick_Forward --> IA_Move_Forward
|
||||||
|
Left_Thumbstick_Backward --> IA_Move_Backward
|
||||||
|
|
||||||
|
What the mapping context buys you is that you can handle
|
||||||
|
events like "IA_Move_Forward" without having to care
|
||||||
|
whether the player is driving with the WASD keys or with
|
||||||
|
the gamepad left thumbstick.
|
||||||
|
|
||||||
|
Typically, enhanced input events go to *all* of the
|
||||||
|
following: the player controller, the character, and
|
||||||
|
user-defined widgets. All of these consumers of enhanced
|
||||||
|
input are automatically registered to receive enhanced
|
||||||
|
input, which means that all they have to do is implement a
|
||||||
|
handler in their event graph, and they're ready. Other
|
||||||
|
actors can *also* receive enhanced input, but that requires
|
||||||
|
jumping through some hoops.
|
||||||
|
|
||||||
|
It's interesting that a widget can implement a handler for a
|
||||||
|
raw keyboard button, and then declare the button "not
|
||||||
|
handled". If the button proceeds to the enhanced input
|
||||||
|
system, and if the widget has a handler for enhanced input,
|
||||||
|
the widget can receive the same button again, in a
|
||||||
|
different form!
|
||||||
|
|
||||||
|
There is a priority order among consumers of enhanced input
|
||||||
|
events: user widgets first (front-to-back), then the player
|
||||||
|
controller, then the character. A consumer of enhanced
|
||||||
|
input has the option of blocking input to lower-priority
|
||||||
|
consumers.
|
||||||
|
|
||||||
|
This is all almost entirely unchanged from Unreal's default
|
||||||
|
behavior. We've only made two tiny tweaks: we send enhanced
|
||||||
|
input to widgets in front-to-back order, and, widgets
|
||||||
|
disable enhanced input by setting a flag instead of by
|
||||||
|
unregistering their input component. Other than that, this
|
||||||
|
is all just stock unreal.
|
||||||
|
|
||||||
|
# Handling mouse buttons
|
||||||
|
|
||||||
|
Mouse buttons behave differently than keyboard buttons.
|
||||||
|
|
||||||
|
Widgets have an OnMouseDown handler. This is only active in
|
||||||
|
point-and-click mode. OnMouseDown only fires when three
|
||||||
|
things are true: the system must be in point-and-click mode,
|
||||||
|
the pointer must be inside the rectangle of a widget, and
|
||||||
|
the widget must be marked hit-testable.
|
||||||
|
|
||||||
|
If no OnMouseDown event fires, or if OnMouseDown declares
|
||||||
|
the mouse down to be "not handled," then the mouse down
|
||||||
|
makes it to the enhanced input subsystem.
|
||||||
|
|
||||||
|
Once the mouse down reaches the enhanced input system, it
|
||||||
|
starts being treated the same as keyboard and gamepad
|
||||||
|
buttons. It can be mapped to an enhanced input event by the
|
||||||
|
input mapping context, and then from there, it can be
|
||||||
|
handled by any enhanced input event handler in a blueprint.
|
||||||
|
|
||||||
|
The upshot of all this is: if you want to think of a mouse
|
||||||
|
button as "just another button," then the way to achieve
|
||||||
|
that is to *not* write an OnMouseDown handler, but instead,
|
||||||
|
to deal with it through enhanced input.
|
||||||
|
|
||||||
|
We have tweaked the default behavior of unreal. If the
|
||||||
|
system is in point-and-click mode, and you click on a widget
|
||||||
|
that is hit-testable, but which has no OnMouseDown handler,
|
||||||
|
we provide a default OnMouseDown behavior: we bring the
|
||||||
|
top-level UserWidget to the front. Because our system grants
|
||||||
|
keyboard focus to the widget in front, this will also grant
|
||||||
|
focus.
|
||||||
|
|
||||||
|
# Handling Mouse Movement
|
||||||
|
|
||||||
|
In point-and-click mode, mouse movement moves the pointer
|
||||||
|
and doesn't generate any events at all.
|
||||||
|
|
||||||
|
There is one exception: mouse capture. If you click on a
|
||||||
|
hit-testable widget, that widget will "capture" the mouse
|
||||||
|
until you release the mouse button. As long as the widget
|
||||||
|
has capture, it receives OnMouseMove events. This is
|
||||||
|
mainly intended to implement click-and-drag, scroll
|
||||||
|
bar scrolling, and other movements like that.
|
||||||
|
|
||||||
|
Unreal has a *lot* of complicated mouse capture and mouse
|
||||||
|
lock options and modes. We don't support any of that. We
|
||||||
|
support only the basics: automatic capture when you click.
|
||||||
|
If you need more, we'll have to improve the Luprex window
|
||||||
|
management system.
|
||||||
|
|
||||||
|
In point-and-click mode, mouse movements do not go to the
|
||||||
|
enhanced input system at all.
|
||||||
|
|
||||||
|
When the system is in mouselook mode, mouse movements go
|
||||||
|
directly to the enhanced input system. They get mapped by
|
||||||
|
the input mapping context and turned into enhanced input
|
||||||
|
events. Handling these events is how mouselook works.
|
||||||
|
|
||||||
|
# Handling Analog Joysticks
|
||||||
|
|
||||||
|
Analog joysticks (including gamepad thumbsticks) generate
|
||||||
|
events that go directly to the enhanced input subsystem.
|
||||||
|
They get mapped to enhanced input events. From there,
|
||||||
|
they can be handled by any consumer of enhanced input.
|
||||||
|
|
||||||
|
# Functions you Should NOT CALL!
|
||||||
|
|
||||||
|
If you're using our Luprex window management system, there are
|
||||||
|
several things your blueprint should *NOT* do:
|
||||||
|
|
||||||
|
- DO NOT use SetKeyboardFocus, SetUserFocus, or any other
|
||||||
|
function with Set-Focus in the name. Instead, just
|
||||||
|
be aware that the frontmost UserWidget will get focus.
|
||||||
|
It can delegate that focus to one of its components by
|
||||||
|
setting DesiredFocusWidget.
|
||||||
|
|
||||||
|
- DO NOT use SetShowMouseCursor, or set the bShowMouseCursor
|
||||||
|
flag. Instead, set the ShowPointer flag in the
|
||||||
|
RootCanvasSlot of any top-level widget.
|
||||||
|
|
||||||
|
- DO NOT use UserWidget::RegisterInputComponent or
|
||||||
|
UserWidget::UnregisterInputComponent. These will be
|
||||||
|
ignored. Instead, set or unset the flag
|
||||||
|
EnableEnhancedInput in the RootCanvasSlot, which
|
||||||
|
effectively does the same thing.
|
||||||
|
|
||||||
|
- DO NOT use SetZOrder. If you try, you will be overridden
|
||||||
|
by our window management code. Currently, the only control
|
||||||
|
we're giving over window z-order is 'BringToFront'. If
|
||||||
|
you need more control, we'll have to enhance the window
|
||||||
|
management system.
|
||||||
|
|
||||||
|
- DO NOT use SetMouseCaptureMode, SetMouseLockMode,
|
||||||
|
SetHideCursorDuringCapture, CaptureMouse, ReleaseMouseCapture,
|
||||||
|
LockMouseToWidget, ReleaseMouseLock. We simply don't support
|
||||||
|
controlling mouse capture and mouse lock at this level of
|
||||||
|
granularity. Trying to use these will fight our window
|
||||||
|
management code. If you need this, we'll have to enhance the
|
||||||
|
window management system.
|
||||||
|
|
||||||
|
- DO NOT use AddToViewport or AddToPlayerScreen. Top level
|
||||||
|
UserWidgets should be inserted into the root canvas using
|
||||||
|
AddWidgetToRoot.
|
||||||
|
|
||||||
|
- DO NOT use SetIgnoreInput. You will be overridden. Our
|
||||||
|
window management system relies on the enhanced input
|
||||||
|
system being active, turning it off would cause everything
|
||||||
|
to fail. However, a widget can handle keyboard or
|
||||||
|
character events, causing them not to be propagated, it
|
||||||
|
can also block events to any widget lower in the z-order,
|
||||||
|
and to the player controller and character.
|
||||||
|
|
||||||
|
- DO NOT use SetInputModeXXX. Be aware that there is no
|
||||||
|
"input mode" enum or "input mode" variable anywhere in
|
||||||
|
Unreal. What these functions actually do is set a large
|
||||||
|
number of state variables - keyboard focus, mouse capture,
|
||||||
|
and so forth - from a single call. Naturally, then, these
|
||||||
|
will fight our window management system.
|
||||||
|
|
||||||
|
# Most *local* event-handling functions are allowed
|
||||||
|
|
||||||
|
There are many functions that gate or route events locally -
|
||||||
|
ie, within a single UserWidget, or within a single Actor.
|
||||||
|
Controlling and gating events within a single localized
|
||||||
|
entity does not create window-management confusion. Because
|
||||||
|
of that, all of these are still allowed:
|
||||||
|
|
||||||
|
- You CAN use EnableInput/DisableInput on actors, to turn
|
||||||
|
enhanced input events on/off for that actor.
|
||||||
|
|
||||||
|
- You CAN use PushInputComponent/PopInputComponent on the
|
||||||
|
player controller, if you want to register something that's
|
||||||
|
NOT a widget to receive enhanced input events. Seems
|
||||||
|
esoteric, but it still works.
|
||||||
|
|
||||||
|
- You CAN use methods of UUserWidget to bind or unbind
|
||||||
|
input events.
|
||||||
|
|
||||||
|
More broadly, functions that an actor or widget uses to
|
||||||
|
manipulate its *own* input component or input events
|
||||||
|
are no problem.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
27
Docs/TASKS/Dictation.txt
Normal file
27
Docs/TASKS/Dictation.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
I need you to act as a secretary taking dictation. You will
|
||||||
|
be helping me to edit a markdown file.
|
||||||
|
|
||||||
|
I have a voice-to-speech program which is running in the background.
|
||||||
|
It records, and it sends my words to you. Most of what I say to you
|
||||||
|
will be meant as text to be put into the markdown file. But
|
||||||
|
occasionally, I will give you verbal instructions, for example:
|
||||||
|
|
||||||
|
"Reformat that bullet list into a numbered list"
|
||||||
|
|
||||||
|
It is your job to figure out intelligently which of the things I
|
||||||
|
say to you is meant as a directive, and which is meant as words to
|
||||||
|
go into the markdown file.
|
||||||
|
|
||||||
|
Do not edit the markdown file after every sentence. Instead,
|
||||||
|
quietly listen until you have a significant edit to make. I'd say,
|
||||||
|
roughly, when you have a paragraph, then it's time to make an edit.
|
||||||
|
|
||||||
|
I may occasionally commandeer the keyboard and edit the markdown
|
||||||
|
file myself. In those cases, you should notice that the file changed,
|
||||||
|
and read my changes.
|
||||||
|
|
||||||
|
It is also your job to make small corrections without comment.
|
||||||
|
If you see a really big mistake, stop and ask me what to do.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
|
|
||||||
* UE Wingman Function Overrides.
|
|
||||||
|
|
||||||
|
|
||||||
* Keyboard Event Handling
|
|
||||||
|
|
||||||
* Menus
|
* Add a slash-command to reload lua source code.
|
||||||
|
|
||||||
* Skeletal Mesh Tangible
|
* 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,3 @@
|
|||||||
--- Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py.orig 2026-04-12 22:58:33.989318455 -0400
|
|
||||||
+++ Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py 2025-11-10 23:34:18.481538118 -0500
|
|
||||||
@@ -32,7 +32,7 @@
|
|
||||||
if DataVal == 0:
|
|
||||||
Val = 'NULL'
|
|
||||||
else:
|
|
||||||
- Expr = '(char16_t*)(%s)' % Data
|
|
||||||
+ Expr = '(char16_t*)(%s)' % DataVal
|
|
||||||
ValRef = valobj.CreateValueFromExpression('string', Expr)
|
|
||||||
Val = ValRef.GetSummary()
|
|
||||||
elif Type.IsReferenceType():
|
|
||||||
@@ -47,6 +47,11 @@
|
|
||||||
Expr = '(char16_t*)(%s)' % valobj.GetAddress()
|
|
||||||
ValRef = valobj.CreateValueFromExpression('string', Expr)
|
|
||||||
Val = ValRef.GetSummary()
|
|
||||||
+ else:
|
|
||||||
+ DataVal = valobj.GetValueAsUnsigned(0)
|
|
||||||
+ Expr = '(char16_t)(%s)' % DataVal
|
|
||||||
+ ValRef = valobj.CreateValueFromExpression('string', Expr)
|
|
||||||
+ Val = ValRef.GetSummary()
|
|
||||||
return Val
|
|
||||||
|
|
||||||
def UESignedCharSummaryProvider(valobj,dict):
|
|
||||||
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-04-12 22:58:34.075320964 -0400
|
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-04-12 22:58:34.075320964 -0400
|
||||||
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-04-12 23:03:40.139226303 -0400
|
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-04-12 23:03:40.139226303 -0400
|
||||||
@@ -149,7 +149,7 @@
|
@@ -149,7 +149,7 @@
|
||||||
286
EnginePatches/EnginePatch-5.7.4
Normal file
286
EnginePatches/EnginePatch-5.7.4
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-05-08 14:11:02.262757499 -0400
|
||||||
|
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-05-05 15:36:35.232152395 -0400
|
||||||
|
@@ -149,7 +149,7 @@
|
||||||
|
FString SolutionDir = GetSolutionPath();
|
||||||
|
TArray<FString> Args;
|
||||||
|
Args.Add(MakePath(SolutionDir));
|
||||||
|
- Args.Add(TEXT("-g ") + MakePath(FullPath) + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber));
|
||||||
|
+ Args.Add(TEXT("-g ") + MakePath(FullPath + FString::Printf(TEXT(":%d:%d"), LineNumber, ColumnNumber)));
|
||||||
|
return Launch(Args);
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp.orig 2026-05-08 14:11:02.370759731 -0400
|
||||||
|
+++ Engine/Source/Editor/UnrealEd/Private/SourceCodeNavigation.cpp 2026-05-05 15:36:35.232353795 -0400
|
||||||
|
@@ -557,7 +557,7 @@
|
||||||
|
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
|
||||||
|
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
|
||||||
|
|
||||||
|
-#if PLATFORM_WINDOWS
|
||||||
|
+#if PLATFORM_WINDOWS || PLATFORM_LINUX
|
||||||
|
FString SourceFileName;
|
||||||
|
uint32 SourceLineNumber = 1;
|
||||||
|
uint32 SourceColumnNumber = 0;
|
||||||
|
@@ -716,8 +716,8 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogSelectionDetails, Warning, TEXT("NavigateToFunctionSource: Unable to look up symbol: %s in module:%s"), *FunctionSymbolName, *FunctionModuleName);
|
||||||
|
-
|
||||||
|
-#endif // PLATFORM_WINDOWS
|
||||||
|
+
|
||||||
|
+#endif // PLATFORM_WINDOWS || PLATFORM_LINUX
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp.orig 2026-05-08 14:11:02.477761942 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxPlatformApplicationMisc.cpp 2026-05-05 15:36:35.232635087 -0400
|
||||||
|
@@ -317,6 +317,9 @@
|
||||||
|
// Furthermore SDL hides the mouse which we prevent by setting SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE, "1"); // When relative mouse mode is active, don't hide cursor.
|
||||||
|
|
||||||
|
+ // Unreal does its own dynamic capturing, we don't need SDL to do it.
|
||||||
|
+ SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
|
||||||
|
+
|
||||||
|
// If we're rendering offscreen, use the "dummy" SDL video driver
|
||||||
|
if (FParse::Param(FCommandLine::Get(), TEXT("RenderOffScreen")) && !getenv("SDL_VIDEODRIVER"))
|
||||||
|
{
|
||||||
|
--- Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp.orig 2026-05-08 14:11:02.584764153 -0400
|
||||||
|
+++ Engine/Source/Runtime/Core/Private/Unix/UnixPlatformStackWalk.cpp 2026-05-05 15:36:35.232750204 -0400
|
||||||
|
@@ -15,6 +15,7 @@
|
||||||
|
#include "HAL/ExceptionHandling.h"
|
||||||
|
#include "HAL/PlatformProcess.h"
|
||||||
|
#include "HAL/PlatformTime.h"
|
||||||
|
+#include "Modules/ModuleManager.h"
|
||||||
|
#include "AutoRTFM.h"
|
||||||
|
|
||||||
|
#include <link.h>
|
||||||
|
@@ -1063,3 +1064,69 @@
|
||||||
|
}
|
||||||
|
ReportLock.Unlock();
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+bool FUnixPlatformStackWalk::GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber)
|
||||||
|
+{
|
||||||
|
+ // Find the .so path for this module.
|
||||||
|
+ FString ModulePath;
|
||||||
|
+ TArray<FModuleStatus> AllModules;
|
||||||
|
+ FModuleManager::Get().QueryModules(AllModules);
|
||||||
|
+ for (const FModuleStatus& Status : AllModules)
|
||||||
|
+ {
|
||||||
|
+ if (FPaths::GetBaseFilename(Status.FilePath) == FunctionModuleName)
|
||||||
|
+ {
|
||||||
|
+ ModulePath = Status.FilePath;
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ if (ModulePath.IsEmpty())
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Debug symbols are in a separate .debug file alongside the .so.
|
||||||
|
+ FString DebugPath = FPaths::ChangeExtension(ModulePath, TEXT("debug"));
|
||||||
|
+ if (!FPaths::FileExists(DebugPath))
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Use lldb to look up the source file and line number.
|
||||||
|
+ // Run: lldb -b -o "image lookup -v -n ClassName::FuncName" <debug_file>
|
||||||
|
+ FString LldbParams = FString::Printf(TEXT("-b -o \"image lookup -v -n %s\" \"%s\""), *FunctionSymbolName, *DebugPath);
|
||||||
|
+ int32 ReturnCode = 0;
|
||||||
|
+ FString AllOutput;
|
||||||
|
+ FString Errors;
|
||||||
|
+ FPlatformProcess::ExecProcess(TEXT("/usr/bin/lldb"), *LldbParams, &ReturnCode, &AllOutput, &Errors);
|
||||||
|
+ if (ReturnCode != 0)
|
||||||
|
+ {
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ // Parse the LineEntry from lldb verbose output.
|
||||||
|
+ // Format: "LineEntry: [0x...-0x...): /path/to/file.cpp:132"
|
||||||
|
+ TArray<FString> Lines;
|
||||||
|
+ AllOutput.ParseIntoArrayLines(Lines);
|
||||||
|
+ for (const FString& Line : Lines)
|
||||||
|
+ {
|
||||||
|
+ FString Trimmed = Line.TrimStartAndEnd();
|
||||||
|
+ if (!Trimmed.StartsWith(TEXT("LineEntry:")))
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
+ int32 ParenIndex = Trimmed.Find(TEXT("): "));
|
||||||
|
+ if (ParenIndex == INDEX_NONE)
|
||||||
|
+ continue;
|
||||||
|
+ FString FileAndLine = Trimmed.Mid(ParenIndex + 3);
|
||||||
|
+
|
||||||
|
+ int32 ColonIndex;
|
||||||
|
+ if (!FileAndLine.FindLastChar(TCHAR(':'), ColonIndex))
|
||||||
|
+ continue;
|
||||||
|
+
|
||||||
|
+ OutPathname = FileAndLine.Left(ColonIndex);
|
||||||
|
+ OutLineNumber = FCString::Atoi(*FileAndLine.Mid(ColonIndex + 1));
|
||||||
|
+ OutColumnNumber = 0;
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return false;
|
||||||
|
+}
|
||||||
|
--- Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h.orig 2026-05-08 14:11:02.692766385 -0400
|
||||||
|
+++ Engine/Source/Runtime/Core/Public/Unix/UnixPlatformStackWalk.h 2026-05-05 15:36:35.232861435 -0400
|
||||||
|
@@ -24,6 +24,8 @@
|
||||||
|
static CORE_API void ThreadStackWalkAndDump(ANSICHAR* HumanReadableString, SIZE_T HumanReadableStringSize, int32 IgnoreCount, uint32 ThreadId);
|
||||||
|
static CORE_API int32 GetProcessModuleCount();
|
||||||
|
static CORE_API int32 GetProcessModuleSignatures(FStackWalkModuleInfo *ModuleSignatures, const int32 ModuleSignaturesSize);
|
||||||
|
+
|
||||||
|
+ static CORE_API bool GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& OutPathname, uint32& OutLineNumber, uint32& OutColumnNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef FUnixPlatformStackWalk FPlatformStackWalk;
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h.orig 2026-05-08 14:11:02.802768658 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Public/Linux/LinuxApplication.h 2026-05-08 14:09:14.778161399 -0400
|
||||||
|
@@ -265,6 +265,12 @@
|
||||||
|
/** The input device Id of the controller that can be used to find the matching ULocalPlayer */
|
||||||
|
FInputDeviceId DeviceId;
|
||||||
|
|
||||||
|
+ /** SDL gamepad type string (e.g. "ps4", "xboxone"), used as HardwareDeviceIdentifier in FInputDeviceScope */
|
||||||
|
+ FName GamepadType;
|
||||||
|
+
|
||||||
|
+ /** SDL gamepad name (human-readable device name), used as InputDeviceName in FInputDeviceScope */
|
||||||
|
+ FName GamepadName;
|
||||||
|
+
|
||||||
|
/** Store axis values from events here to be handled once per frame. */
|
||||||
|
TMap<FGamepadKeyNames::Type, float> AxisEvents;
|
||||||
|
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp.orig 2026-05-08 14:11:02.910770890 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp 2026-05-08 14:09:14.778345206 -0400
|
||||||
|
@@ -13,6 +13,13 @@
|
||||||
|
#include "IHapticDevice.h"
|
||||||
|
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
|
||||||
|
|
||||||
|
+namespace UE::LinuxInput
|
||||||
|
+{
|
||||||
|
+ static const FName InputClassName = TEXT("LinuxApplication");
|
||||||
|
+ static const FString KBMInputHardwareName = TEXT("KBM");
|
||||||
|
+ static const FString TouchInputHardwareName = TEXT("MobileTouch");
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
//
|
||||||
|
// GameController thresholds
|
||||||
|
//
|
||||||
|
@@ -320,6 +327,7 @@
|
||||||
|
{
|
||||||
|
case SDL_EVENT_KEY_DOWN:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
const SDL_KeyboardEvent &KeyEvent = Event.key;
|
||||||
|
SDL_Keycode KeySym = KeyEvent.key;
|
||||||
|
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
|
||||||
|
@@ -342,6 +350,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_KEY_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
const SDL_KeyboardEvent &KeyEvent = Event.key;
|
||||||
|
const SDL_Keycode KeySym = KeyEvent.key;
|
||||||
|
const uint32 CharCode = CharCodeFromSDLKeySym(KeySym);
|
||||||
|
@@ -352,6 +361,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_TEXT_INPUT:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
// Slate now gets all its text from here, I hope.
|
||||||
|
const bool bIsRepeated = false; //Event.key.repeat != 0;
|
||||||
|
const FString TextStr(UTF8_TO_TCHAR(Event.text.text));
|
||||||
|
@@ -363,6 +373,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseMotionEvent motionEvent = Event.motion;
|
||||||
|
FLinuxCursor *LinuxCursor = (FLinuxCursor*)Cursor.Get();
|
||||||
|
LinuxCursor->InvalidateCaches();
|
||||||
|
@@ -406,6 +417,7 @@
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseButtonEvent buttonEvent = Event.button;
|
||||||
|
|
||||||
|
EMouseButtons::Type button;
|
||||||
|
@@ -484,6 +496,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_WHEEL:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::KBMInputHardwareName);
|
||||||
|
SDL_MouseWheelEvent *WheelEvent = &Event.wheel;
|
||||||
|
float Amount = (float)WheelEvent->y * fMouseWheelScrollAccel;
|
||||||
|
|
||||||
|
@@ -515,6 +528,7 @@
|
||||||
|
|
||||||
|
SDLControllerState &ControllerState = ControllerStates[caxisEvent.which];
|
||||||
|
FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerState.DeviceId);
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ControllerState.GamepadName, ControllerState.DeviceId.GetId(), ControllerState.GamepadType.ToString());
|
||||||
|
|
||||||
|
switch (caxisEvent.axis)
|
||||||
|
{
|
||||||
|
@@ -740,15 +754,17 @@
|
||||||
|
|
||||||
|
if (Button != FGamepadKeyNames::Invalid)
|
||||||
|
{
|
||||||
|
- FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ControllerStates[cbuttonEvent.which].DeviceId);
|
||||||
|
+ SDLControllerState& ButtonControllerState = ControllerStates[cbuttonEvent.which];
|
||||||
|
+ FPlatformUserId UserId = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(ButtonControllerState.DeviceId);
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ButtonControllerState.GamepadName, ButtonControllerState.DeviceId.GetId(), ButtonControllerState.GamepadType.ToString());
|
||||||
|
|
||||||
|
if(cbuttonEvent.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN)
|
||||||
|
{
|
||||||
|
- MessageHandler->OnControllerButtonPressed(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
|
||||||
|
+ MessageHandler->OnControllerButtonPressed(Button, UserId, ButtonControllerState.DeviceId, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
- MessageHandler->OnControllerButtonReleased(Button, UserId, ControllerStates[cbuttonEvent.which].DeviceId, false);
|
||||||
|
+ MessageHandler->OnControllerButtonReleased(Button, UserId, ButtonControllerState.DeviceId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1018,6 +1034,7 @@
|
||||||
|
|
||||||
|
case SDL_EVENT_FINGER_DOWN:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is down at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
|
||||||
|
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
@@ -1053,6 +1070,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_FINGER_UP:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
UE_LOG(LogLinuxWindow, Verbose, TEXT("Finger %llu is up at (%f, %f)"), Event.tfinger.fingerID, Event.tfinger.x, Event.tfinger.y);
|
||||||
|
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
@@ -1087,6 +1105,7 @@
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_FINGER_MOTION:
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, UE::LinuxInput::InputClassName, IPlatformInputDeviceMapper::Get().GetDefaultInputDevice().GetId(), UE::LinuxInput::TouchInputHardwareName);
|
||||||
|
// touch events can have no window associated with them, in that case ignore (with a warning)
|
||||||
|
if (LIKELY(!bWindowlessEvent))
|
||||||
|
{
|
||||||
|
@@ -1196,6 +1215,7 @@
|
||||||
|
IPlatformInputDeviceMapper& Mapper = IPlatformInputDeviceMapper::Get();
|
||||||
|
for(auto ControllerIt = ControllerStates.CreateIterator(); ControllerIt; ++ControllerIt)
|
||||||
|
{
|
||||||
|
+ FInputDeviceScope InputScope(nullptr, ControllerIt.Value().GamepadName, ControllerIt.Value().DeviceId.GetId(), ControllerIt.Value().GamepadType.ToString());
|
||||||
|
for(auto Event = ControllerIt.Value().AxisEvents.CreateConstIterator(); Event; ++Event)
|
||||||
|
{
|
||||||
|
FPlatformUserId UserId = Mapper.GetUserForInputDevice(ControllerIt.Value().DeviceId);
|
||||||
|
@@ -2075,6 +2095,9 @@
|
||||||
|
UE_LOG(LogLinux, Verbose, TEXT("Adding controller %i '%s'"), FirstUnusedIndex, UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
|
||||||
|
auto& ControllerState = ControllerStates.Add(Id);
|
||||||
|
ControllerState.Controller = Controller;
|
||||||
|
+ ControllerState.GamepadName = FName(UTF8_TO_TCHAR(SDL_GetGamepadName(Controller)));
|
||||||
|
+ const char* GamepadTypeStr = SDL_GetGamepadStringForType(SDL_GetGamepadType(Controller));
|
||||||
|
+ ControllerState.GamepadType = FName(GamepadTypeStr ? UTF8_TO_TCHAR(GamepadTypeStr) : TEXT("unknown"));
|
||||||
|
|
||||||
|
FPlatformUserId UserId = FPlatformUserId::CreateFromInternalId(FirstUnusedIndex);
|
||||||
|
DeviceMapper.RemapControllerIdToPlatformUserAndDevice(FirstUnusedIndex, UserId, ControllerState.DeviceId);
|
||||||
47
EnginePatches/Old-Patches.md
Normal file
47
EnginePatches/Old-Patches.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Old Patches
|
||||||
|
|
||||||
|
Patches that were once part of `EnginePatch` but are no longer needed.
|
||||||
|
Kept here for reference in case the underlying issue resurfaces.
|
||||||
|
|
||||||
|
## LinuxWindow.cpp — force override_redirect for borderless child windows
|
||||||
|
|
||||||
|
Removed when the engine was upgraded to UE 5.7, which uses SDL3. SDL3 has
|
||||||
|
better Wayland support, so this workaround is believed to be unnecessary.
|
||||||
|
The original purpose was to make Unreal play better with Wayland: under
|
||||||
|
XWayland, non-override-redirect popup windows don't receive input events
|
||||||
|
from the compositor. SDL already sets override_redirect for tooltips and
|
||||||
|
popup menus, but other borderless child windows (notification popups,
|
||||||
|
dialogs) also needed it, so we temporarily enabled
|
||||||
|
`SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT` around the `SDL_CreateWindow` call.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
--- Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp.orig 2026-04-12 22:58:34.538334467 -0400
|
||||||
|
+++ Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxWindow.cpp 2026-04-12 22:48:15.848291098 -0400
|
||||||
|
@@ -235,7 +235,26 @@
|
||||||
|
|
||||||
|
// The SDL window doesn't need to be reshaped.
|
||||||
|
// the size of the window you input is the sizeof the client.
|
||||||
|
+
|
||||||
|
+ // Under XWayland, non-override-redirect popup windows don't receive input
|
||||||
|
+ // events from the compositor. SDL already sets override_redirect for
|
||||||
|
+ // tooltips and popup menus, but other borderless child windows (like
|
||||||
|
+ // notification popups and dialogs) also need it. We temporarily enable
|
||||||
|
+ // the SDL hint to force override_redirect for these windows.
|
||||||
|
+ bool bForceOverrideRedirect = !Definition->HasOSWindowBorder
|
||||||
|
+ && InParent.IsValid()
|
||||||
|
+ && !(WindowStyle & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_POPUP_MENU));
|
||||||
|
+ if (bForceOverrideRedirect)
|
||||||
|
+ {
|
||||||
|
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "1");
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
HWnd = SDL_CreateWindow( TCHAR_TO_ANSI( *Definition->Title ), X, Y, ClientWidth, ClientHeight, WindowStyle );
|
||||||
|
+
|
||||||
|
+ if (bForceOverrideRedirect)
|
||||||
|
+ {
|
||||||
|
+ SDL_SetHint(SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT, "0");
|
||||||
|
+ }
|
||||||
|
// produce a helpful message for common driver errors
|
||||||
|
if (HWnd == nullptr)
|
||||||
|
{
|
||||||
|
```
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"files.watcherExclude": {
|
"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,
|
||||||
@@ -123,10 +127,11 @@
|
|||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"initCommands": [
|
"initCommands": [
|
||||||
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
|
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
|
||||||
"settings set target.inline-breakpoint-strategy always",
|
"settings set target.inline-breakpoint-strategy always",
|
||||||
|
"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()\""
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -146,8 +151,10 @@
|
|||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"initCommands": [
|
"initCommands": [
|
||||||
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
|
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
|
||||||
"settings set target.inline-breakpoint-strategy always"
|
"settings set target.inline-breakpoint-strategy always",
|
||||||
|
"settings set target.prefer-dynamic-value no-run-target",
|
||||||
|
"process handle SIGTRAP --notify false --pass false --stop false"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -160,7 +167,9 @@
|
|||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"initCommands": [
|
"initCommands": [
|
||||||
"settings set target.inline-breakpoint-strategy always"
|
"settings set target.inline-breakpoint-strategy always",
|
||||||
|
"settings set target.prefer-dynamic-value no-run-target",
|
||||||
|
"process handle SIGTRAP --notify false --pass false --stop false"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials.
|
|||||||
|
|
||||||
## How Does it Work?
|
## 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 "ue-wingman-mcp.py", which an AI
|
The python program is actually less than 100 lines of code:
|
||||||
can use to send commands to the plugin.
|
all it does is package up its command line arguments, send
|
||||||
|
them to the plugin, and let the plugin do the work. Then it
|
||||||
|
prints the output.
|
||||||
|
|
||||||
If you build Unreal from source, the best way to install the
|
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,13 +12,21 @@ class UWing_Documentation_Manual : public UWingHandler
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this, TEXT("Print the entire manual."));
|
||||||
TEXT("Print the user manual."));
|
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
WingManual::PrintManual({WingManual::Section::All}, nullptr, false);
|
UWingManualSections::FetcherPaths();
|
||||||
|
UWingManualSections::ExpressingTypes();
|
||||||
|
UWingManualSections::VariableDeclarations();
|
||||||
|
UWingManualSections::EscapeSequencesInFNames();
|
||||||
|
UWingManualSections::MaterialEditing();
|
||||||
|
UWingManualSections::NodeContextMenus();
|
||||||
|
UWingManualSections::VariableGettersAndSetters();
|
||||||
|
UWingManualSections::BestPerformance();
|
||||||
|
UWingManualSections::ImportantCommands();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "WingBasics.h"
|
||||||
|
#include "WingManual.h"
|
||||||
|
#include "WingServer.h"
|
||||||
|
#include "Documentation_Section.generated.h"
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UWing_Documentation_Section : public UWingHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(EditAnywhere, meta=(Description="Manual section"))
|
||||||
|
FString Section;
|
||||||
|
|
||||||
|
virtual void Register() override
|
||||||
|
{
|
||||||
|
TStringBuilder<128> Docs;
|
||||||
|
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
|
||||||
|
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
|
||||||
|
UWingServer::AddHandler(this, Docs.ToString());
|
||||||
|
}
|
||||||
|
virtual void Handle() override
|
||||||
|
{
|
||||||
|
FName SectionName(*Section);
|
||||||
|
if (WingManual::PrintSection(SectionName))
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT("\n"));
|
||||||
|
WingManual::PrintSectionNames(TEXT("Other manual sections:"), WingManual::GetSections(), WingOut::Stdout);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
|
||||||
|
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), WingManual::GetSections(), WingOut::Stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -28,13 +28,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
|
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;
|
|
||||||
FSpawnNodeEntry Entry;
|
|
||||||
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
|
|
||||||
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
|
|
||||||
{
|
|
||||||
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;
|
|
||||||
TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.Type, 2, true);
|
|
||||||
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Entry.Type, WingOut::Stdout)) return;
|
|
||||||
Entry.Action = Results[0];
|
|
||||||
Entries.Add(Entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute all.
|
|
||||||
for (const FSpawnNodeEntry &Entry : Entries)
|
|
||||||
{
|
|
||||||
UEdGraphNode* NewNode = Entry.Action->Execute(FVector2D(Entry.PosX, Entry.PosY));
|
|
||||||
if (NewNode)
|
if (NewNode)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Entry.Type);
|
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Type);
|
||||||
WingGraphExport Export(NewNode, false, true);
|
WingGraphExport Export(NewNode, false, true);
|
||||||
WingOut::Stdout.Print(Export.GetOutput());
|
WingOut::Stdout.Print(Export.GetOutput());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
WingOut::Stdout.Printf(TEXT("Failed: %s\n"), *Type);
|
||||||
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="Query string, can contain * wildcards"))
|
|
||||||
FString Query;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
|
|
||||||
int32 MaxResults = 50;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
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,
|
||||||
@@ -41,6 +41,9 @@ public:
|
|||||||
if (!TargetGraph) return;
|
if (!TargetGraph) return;
|
||||||
|
|
||||||
FWingGraphActions GraphActions(TargetGraph);
|
FWingGraphActions GraphActions(TargetGraph);
|
||||||
|
for (const FString& Query : Queries.Argv)
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||||
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
|
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
|
||||||
for (const FWingGraphAction* Action : Results)
|
for (const FWingGraphAction* Action : Results)
|
||||||
{
|
{
|
||||||
@@ -56,4 +59,5 @@ public:
|
|||||||
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,13 +20,15 @@ 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
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Connect pins between nodes in a graph (Blueprint or Material)."));
|
TEXT("Connect pins between nodes in a graph (Blueprint or Material). "
|
||||||
|
"Pin IDs use fetcher path syntax relative to the graph, eg: "
|
||||||
|
"node:K2Node_CallFunction_0,pin:ReturnValue"));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -52,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"
|
||||||
@@ -15,19 +14,6 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FDisconnectPinEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Pin;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional))
|
|
||||||
FString TargetPin;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class UWing_GraphPin_Disconnect : public UWingHandler
|
class UWing_GraphPin_Disconnect : public UWingHandler
|
||||||
{
|
{
|
||||||
@@ -37,14 +23,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
|
||||||
FString Graph;
|
FString Graph;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Description="Array of {pin, targetPin?} objects. If targetPin is omitted, all connections on the pin are broken."))
|
UPROPERTY(EditAnywhere, meta=(Description="Pin ID strings"))
|
||||||
FWingJsonArray Disconnections;
|
FWingRestOfArgv Pins;
|
||||||
|
|
||||||
virtual void Register() override
|
virtual void Register() override
|
||||||
{
|
{
|
||||||
UWingServer::AddHandler(this,
|
UWingServer::AddHandler(this,
|
||||||
TEXT("Disconnect pins in a graph (Blueprint or Material). "
|
TEXT("Disconnect all connections on the specified pins. "
|
||||||
"Can disconnect a specific link or all links on a pin."));
|
"Pin IDs use fetcher path syntax relative to the graph, eg: "
|
||||||
|
"node:K2Node_CallFunction_0,pin:ReturnValue"));
|
||||||
}
|
}
|
||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
@@ -55,47 +42,19 @@ public:
|
|||||||
int32 SuccessCount = 0;
|
int32 SuccessCount = 0;
|
||||||
int32 TotalDisconnected = 0;
|
int32 TotalDisconnected = 0;
|
||||||
|
|
||||||
FDisconnectPinEntry Entry;
|
for (const FString& PinPath : Pins.Argv)
|
||||||
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry);
|
|
||||||
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
|
|
||||||
{
|
{
|
||||||
if (!FWingProperty::PopulateFromJson(EntryProps, *DiscVal, false, WingOut::Stdout)) continue;
|
|
||||||
|
|
||||||
WingFetcher FP(G, WingOut::Stdout);
|
WingFetcher FP(G, WingOut::Stdout);
|
||||||
UWingGraphPinRef* PinRef = FP.Walk(Entry.Pin).Cast<UWingGraphPinRef>();
|
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
|
||||||
if (!PinRef) continue;
|
if (!PinRef) continue;
|
||||||
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
|
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
|
||||||
if (!Pin) continue;
|
if (!Pin) continue;
|
||||||
|
|
||||||
int32 DisconnectedCount = 0;
|
int32 DisconnectedCount = Pin->LinkedTo.Num();
|
||||||
|
|
||||||
if (!Entry.TargetPin.IsEmpty())
|
|
||||||
{
|
|
||||||
WingFetcher FT(G, WingOut::Stdout);
|
|
||||||
UWingGraphPinRef* TargetRef = FT.Walk(Entry.TargetPin).Cast<UWingGraphPinRef>();
|
|
||||||
if (!TargetRef) continue;
|
|
||||||
UEdGraphPin* Target = WingUtils::CheckGetPin(TargetRef->Node, TargetRef->PinName, WingOut::Stdout);
|
|
||||||
if (!Target) continue;
|
|
||||||
|
|
||||||
if (!Pin->LinkedTo.Contains(Target))
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT("Error: %s.%s is not connected to %s.%s\n"),
|
|
||||||
*WingUtils::FormatName(Pin->GetOwningNode()), *WingUtils::FormatName(Pin),
|
|
||||||
*WingUtils::FormatName(Target->GetOwningNode()), *WingUtils::FormatName(Target));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pin->BreakLinkTo(Target);
|
|
||||||
DisconnectedCount = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DisconnectedCount = Pin->LinkedTo.Num();
|
|
||||||
if (DisconnectedCount > 0)
|
if (DisconnectedCount > 0)
|
||||||
{
|
{
|
||||||
Pin->BreakAllPinLinks(true);
|
Pin->BreakAllPinLinks(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
WingOut::Stdout.Printf(TEXT("Disconnected %d link(s) from %s.%s\n"),
|
WingOut::Stdout.Printf(TEXT("Disconnected %d link(s) from %s.%s\n"),
|
||||||
DisconnectedCount,
|
DisconnectedCount,
|
||||||
@@ -105,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, Disconnections.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
|
||||||
|
|||||||
59
Plugins/UEWingman/Source/UEWingman/Handlers/Test_TMaps.h
Normal file
59
Plugins/UEWingman/Source/UEWingman/Handlers/Test_TMaps.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "WingServer.h"
|
||||||
|
#include "WingBasics.h"
|
||||||
|
#include "Containers/BitArray.h"
|
||||||
|
#include "Containers/SparseArray.h"
|
||||||
|
#include "Test_TMaps.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UWing_Test_TMaps : public UWingHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void Register() override
|
||||||
|
{
|
||||||
|
UWingServer::AddHandler(this,
|
||||||
|
TEXT("Constructs a small TMap, TBitArray, and TSparseArray so that "
|
||||||
|
"a developer can set a breakpoint and inspect them with the "
|
||||||
|
"lldb data formatters."));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle() override
|
||||||
|
{
|
||||||
|
TMap<int32, FString> Map;
|
||||||
|
Map.Add(1, TEXT("one"));
|
||||||
|
Map.Add(2, TEXT("two"));
|
||||||
|
Map.Add(3, TEXT("three"));
|
||||||
|
Map.Add(42, TEXT("forty-two"));
|
||||||
|
|
||||||
|
TBitArray<> Bits;
|
||||||
|
Bits.Add(true);
|
||||||
|
Bits.Add(false);
|
||||||
|
Bits.Add(true);
|
||||||
|
Bits.Add(true);
|
||||||
|
Bits.Add(false);
|
||||||
|
|
||||||
|
// Add a few entries, then remove a middle one so the live set is
|
||||||
|
// non-contiguous. Exercises the sparse-array formatter against a hole.
|
||||||
|
TSparseArray<FString> Sparse;
|
||||||
|
Sparse.Add(TEXT("alpha"));
|
||||||
|
int32 BetaIdx = Sparse.Add(TEXT("beta"));
|
||||||
|
Sparse.Add(TEXT("gamma"));
|
||||||
|
Sparse.Add(TEXT("delta"));
|
||||||
|
Sparse.RemoveAt(BetaIdx);
|
||||||
|
|
||||||
|
TTuple<int32, FString, float, bool, FName> Tuple(1, TEXT("hello"), 3.14f, true, FName("world"));
|
||||||
|
|
||||||
|
// Set a breakpoint on the following line to inspect Map, Bits, and Sparse.
|
||||||
|
WingOut::Stdout.Printf(TEXT("Test_TMaps: Map has %d entries, Bits has %d bits, Sparse has %d entries.\n"),
|
||||||
|
Map.Num(), Bits.Num(), Sparse.Num());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
|
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="Query string, can contain *"))
|
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
|
||||||
FString Query;
|
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (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,
|
||||||
@@ -32,6 +32,9 @@ public:
|
|||||||
virtual void Handle() override
|
virtual void Handle() override
|
||||||
{
|
{
|
||||||
WingWidgets Widgets;
|
WingWidgets Widgets;
|
||||||
|
for (const FString& Query : Queries.Argv)
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
|
||||||
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
|
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
|
||||||
for (const WingWidgets::Type& Entry : Results)
|
for (const WingWidgets::Type& Entry : Results)
|
||||||
{
|
{
|
||||||
@@ -47,4 +50,5 @@ public:
|
|||||||
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -53,7 +54,7 @@ void WingFetcher::PathFailed(const TCHAR* Expected)
|
|||||||
Errors.Printf(TEXT("ERROR: Path specifies a %s, but expected %s\n"), *Obj.Get()->GetClass()->GetName(), Expected);
|
Errors.Printf(TEXT("ERROR: Path specifies a %s, but expected %s\n"), *Obj.Get()->GetClass()->GetName(), Expected);
|
||||||
else
|
else
|
||||||
Errors.Printf(TEXT("ERROR: Path led to a null pointer\n"));
|
Errors.Printf(TEXT("ERROR: Path led to a null pointer\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
SetError();
|
SetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ WingFetcher& WingFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expecte
|
|||||||
Errors.Printf(TEXT("ERROR: Input to '%s' is %s, but expected %s\n"), Walker, *Obj.Get()->GetClass()->GetName(), Expected);
|
Errors.Printf(TEXT("ERROR: Input to '%s' is %s, but expected %s\n"), Walker, *Obj.Get()->GetClass()->GetName(), Expected);
|
||||||
else
|
else
|
||||||
Errors.Printf(TEXT("ERROR: Path led to a null pointer\n"));
|
Errors.Printf(TEXT("ERROR: Path led to a null pointer\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
SetError();
|
SetError();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -75,8 +76,8 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
|
|||||||
if (Path.Contains(TEXT(" ")))
|
if (Path.Contains(TEXT(" ")))
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Paths may not contain whitespace."));
|
Errors.Printf(TEXT("ERROR: Paths may not contain whitespace."));
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
UWingServer::SuggestManual(WingManual::Section::EscapeSequences);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, EscapeSequencesInFNames));
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
TArray<FString> Segments;
|
TArray<FString> Segments;
|
||||||
@@ -84,7 +85,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
|
|||||||
if (Segments.Num() == 0)
|
if (Segments.Num() == 0)
|
||||||
{
|
{
|
||||||
Errors.Print(TEXT("ERROR: Empty path\n"));
|
Errors.Print(TEXT("ERROR: Empty path\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
|
|||||||
if (!Func)
|
if (!Func)
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key);
|
Errors.Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key);
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
(this->*Func)(Value);
|
(this->*Func)(Value);
|
||||||
@@ -122,7 +123,7 @@ WingFetcher& WingFetcher::Asset(const FString& PackagePath)
|
|||||||
if (!PackagePath.StartsWith(TEXT("/")))
|
if (!PackagePath.StartsWith(TEXT("/")))
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Path must start with '/', got '%s'\n"), *PackagePath);
|
Errors.Printf(TEXT("ERROR: Path must start with '/', got '%s'\n"), *PackagePath);
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ WingFetcher& WingFetcher::Graph(const FString& Value)
|
|||||||
if (!Value.IsEmpty())
|
if (!Value.IsEmpty())
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n"));
|
Errors.Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::Paths);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, FetcherPaths));
|
||||||
return SetError();
|
return SetError();
|
||||||
}
|
}
|
||||||
WingUtils::EnsureMaterialGraph(Mat);
|
WingUtils::EnsureMaterialGraph(Mat);
|
||||||
@@ -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;
|
|
||||||
HostBase = SPtr->StructBase;
|
|
||||||
HostType = SPtr->StructType;
|
|
||||||
HostEditable = SPtr->Editable;
|
|
||||||
}
|
|
||||||
|
|
||||||
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
|
|
||||||
if (!StructProp)
|
|
||||||
{
|
|
||||||
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->GetName());
|
|
||||||
return SetError();
|
|
||||||
}
|
|
||||||
|
|
||||||
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
UWingStructRef* Ptr = NewObject<UWingStructRef>();
|
||||||
Ptr->Object = HostObject;
|
Ptr->Object = WProp->Object.Get();
|
||||||
Ptr->StructType = StructProp->Struct;
|
Ptr->StructType = FSProp->Struct;
|
||||||
Ptr->StructBase = StructProp->ContainerPtrToValuePtr<void>(HostBase);
|
Ptr->StructBase = FSProp->ContainerPtrToValuePtr<void>(WProp->Container);
|
||||||
Ptr->Editable = HostEditable && StructProp->HasAllPropertyFlags(CPF_Edit);
|
Ptr->Editable = WProp->Editable;
|
||||||
SetObj(Ptr);
|
SetObj(Ptr);
|
||||||
return *this;
|
return *this;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Errors.Printf(TEXT("Property %s is not a struct property.\n"),
|
||||||
|
*WProp->Prop->GetName());
|
||||||
|
return SetError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,23 @@
|
|||||||
|
|
||||||
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
|
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());
|
||||||
}
|
}
|
||||||
WingOut::Stdout.Print(TEXT(")\n"));
|
else
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
|
||||||
@@ -26,27 +32,17 @@ 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)
|
||||||
@@ -58,42 +54,14 @@ void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
|
|||||||
WingOut::Stdout.Print(TEXT("\n"));
|
WingOut::Stdout.Print(TEXT("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* Handler, bool Abridged)
|
void UWingManualSections::FetcherPaths()
|
||||||
{
|
{
|
||||||
if (Sections.IsEmpty()) return;
|
|
||||||
|
|
||||||
const bool bPrintAll = Sections.Contains(Section::All);
|
|
||||||
|
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Printf(TEXT("\n--- AUTOMATIC DOCUMENTATION ---\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Handler && (Sections.Contains(Section::HandlerHelp) || bPrintAll))
|
|
||||||
{
|
|
||||||
PrintHandlerHelp(*Handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::Paths) || bPrintAll)
|
|
||||||
{
|
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n PATHS: Here are some example paths:"
|
"\n FETCHER PATHS:"
|
||||||
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas·122"
|
|
||||||
"\n /Game/Testing/BP_Test,graph:Rescale·Actor,node:K2Node_CallFunction_0,pin:Scale"
|
|
||||||
"\n /Game/Chars/BP_Manny,component:Camera·Boom"
|
|
||||||
"\n"
|
"\n"
|
||||||
));
|
"\n Most commands require you to specify a 'fetcher path'."
|
||||||
}
|
"\n A fetcher path starts with an asset name, followed by"
|
||||||
else
|
"\n steps that navigate into the asset. Some Examples:"
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n PATHS:"
|
|
||||||
"\n"
|
|
||||||
"\n Most commands require you to specify a path. A path starts"
|
|
||||||
"\n with an asset name, followed by steps separated by ,"
|
|
||||||
"\n 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"
|
||||||
@@ -107,9 +75,10 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
|
|||||||
"\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"
|
"\n"
|
||||||
"\n Notice that paths use sanitized identifiers. See the section"
|
"\n Notice that paths use escaped fnames. See the section"
|
||||||
"\n on identifier sanitization below for 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:"
|
||||||
@@ -117,29 +86,15 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
|
|||||||
"\n /Game/Materials/MyMaterial,graph"
|
"\n /Game/Materials/MyMaterial,graph"
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::Types) || bPrintAll)
|
void UWingManualSections::ExpressingTypes()
|
||||||
{
|
{
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n TYPES: Here are some examples of valid types:"
|
"\n EXPRESSING TYPES:"
|
||||||
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
|
|
||||||
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
|
|
||||||
"\n Array<Int>, Set<String>, Map<Int,Actor>"
|
|
||||||
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
|
|
||||||
"\n"
|
"\n"
|
||||||
));
|
"\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:"
|
||||||
else
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n TYPES:"
|
|
||||||
"\n"
|
|
||||||
"\n To change variable types, or to express function prototypes, you will"
|
|
||||||
"\n 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,"
|
||||||
@@ -148,79 +103,49 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
|
|||||||
"\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 BP_Foo."
|
"\n is used as a type, the typename is just BP_Foo. You can search"
|
||||||
|
"\n for valid types using the TypeName_Search command."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::VariableDeclarations) || bPrintAll)
|
void UWingManualSections::VariableDeclarations()
|
||||||
{
|
{
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n VARIABLE DECLARATIONS: example variable declarations:"
|
|
||||||
"\n Array<Actor> Actors"
|
|
||||||
"\n Float F (InstanceEditable)"
|
|
||||||
"\n String S = This is the default value"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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 always on one line:"
|
|
||||||
"\n"
|
"\n"
|
||||||
"\n Array<Actor> Actors"
|
"\n kind type name (optional flags) = optional default value"
|
||||||
"\n Float F (InstanceEditable)"
|
"\n"
|
||||||
"\n String S = This is the default value"
|
"\n Kind can be:"
|
||||||
|
"\n"
|
||||||
|
"\n blueprint - eg, instance variables"
|
||||||
|
"\n input - a function argument or macro input"
|
||||||
|
"\n output - a function return value or macro output"
|
||||||
|
"\n local - local variables"
|
||||||
|
"\n"
|
||||||
|
"\n Here are some examples:"
|
||||||
|
"\n"
|
||||||
|
"\n input Array<Actor> Actors"
|
||||||
|
"\n output Float F (InstanceEditable)"
|
||||||
|
"\n blueprint String S = This is the default value"
|
||||||
"\n"
|
"\n"
|
||||||
"\n The commands Variables_Add, Variables_Modify,"
|
"\n The commands Variables_Add, Variables_Modify,"
|
||||||
"\n and Variables_Remove can be used to edit: "
|
"\n and Variables_Remove can be used to edit "
|
||||||
"\n blueprint variables, graph local variables, graph input"
|
"\n blueprint variables, graph local variables, graph input"
|
||||||
"\n variables, graph output variables, and custom"
|
"\n variables, graph output variables, and custom"
|
||||||
"\n event node input variables. Event dispatchers are"
|
"\n event node input variables. Event dispatchers are"
|
||||||
"\n also graphs, so they too can be edited."
|
"\n also graphs, so they too can be edited."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::EscapeSequences) || bPrintAll)
|
void UWingManualSections::EscapeSequencesInFNames()
|
||||||
{
|
{
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n USING HTML ESCAPE SEQUENCES:"
|
"\n ESCAPE SEQUENCES IN FNAMES:"
|
||||||
"\n When we output FNames, we use HTML escape sequences for the"
|
|
||||||
"\n following marks: \\\"'(),.:;<=>& We also escape control"
|
|
||||||
"\n characters, and some (but not all) unicode characters."
|
|
||||||
"\n We also translate spaces to periods. Examples:"
|
|
||||||
"\n"
|
"\n"
|
||||||
"\n FName(TEXT(\"No_Change\")) --> No_Change"
|
|
||||||
"\n FName(TEXT(\"ίδιος\")) --> ίδιος"
|
|
||||||
"\n FName(TEXT(\"✡✢❄\")) --> ✡✢❄"
|
|
||||||
"\n FName(TEXT(\"Hello.There\")) --> Hello.There"
|
|
||||||
"\n FName(TEXT(\"Hello There\")) --> Hello.There"
|
|
||||||
"\n FName(TEXT(\"Hello\n\")) --> Hello
"
|
|
||||||
"\n "
|
|
||||||
"\n When sending FNames to UE Wingman, you *must* escape the marks"
|
|
||||||
"\n listed above and control characters, but you *may* escape"
|
|
||||||
"\n any character. To send an FName with a space in it, either"
|
|
||||||
"\n use   or a period."
|
|
||||||
"\n"
|
|
||||||
"\n Currently, this escaping *only* applies to FNames. It"
|
|
||||||
"\n doesn't work to use escapes in asset names or file names."
|
|
||||||
"\n"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n USING HTML ESCAPE SEQUENCES:"
|
|
||||||
"\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."
|
||||||
@@ -230,34 +155,10 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
|
|||||||
"\n with a space in it, either use   or a period."
|
"\n with a space in it, either use   or a period."
|
||||||
"\n"
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::Whitespace) || bPrintAll)
|
void UWingManualSections::MaterialEditing()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n ABOUT WHITESPACE:"
|
|
||||||
"\n Do not put excess whitespace into paths, typenames, or"
|
|
||||||
"\n function prototypes, only use whitespace where it is required"
|
|
||||||
"\n by the syntax."
|
|
||||||
"\n"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::MaterialEditing) || bPrintAll)
|
|
||||||
{
|
|
||||||
if (Abridged)
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
|
||||||
"\n MATERIAL EDITING:"
|
|
||||||
"\n We do not expose material expressions directly. Instead, use"
|
|
||||||
"\n Details_Dump and Details_Set on the material graph nodes to"
|
|
||||||
"\n edit material expression properties."
|
|
||||||
"\n"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n MATERIAL EDITING:"
|
"\n MATERIAL EDITING:"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -267,32 +168,102 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
|
|||||||
"\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 a single node what would otherwise take many.\n"
|
||||||
|
"\n"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Sections.Contains(Section::ImportantCommands) || bPrintAll)
|
void UWingManualSections::NodeContextMenus()
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Print(TEXT(
|
WingOut::Stdout.Print(TEXT(
|
||||||
"\n COMMANDS YOU SHOULD KNOW ABOUT AND REMEMBER:"
|
"\n NODE CONTEXT MENUS:"
|
||||||
"\n Documentation_Manual: this explanation"
|
"\n"
|
||||||
"\n Documentation_Commands: a list of all the main commands"
|
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
|
||||||
"\n Documentation_CreateAssets: Additional commands that create new assets"
|
"\n to the node context menu. This menu includes both node"
|
||||||
|
"\n operations and pin operations (e.g. Split Struct Pin,"
|
||||||
|
"\n Add Pin)."
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWingManualSections::VariableGettersAndSetters()
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT(
|
||||||
|
"\n VARIABLE GETTERS AND SETTERS:"
|
||||||
|
"\n"
|
||||||
|
"\n Access to local vars, function parameters, and "
|
||||||
|
"\n blueprint vars is through getter and setter nodes. "
|
||||||
|
"\n These can be found in GraphNode_SearchTypes by "
|
||||||
|
"\n searching for 'Variable'. Some examples:"
|
||||||
|
"\n"
|
||||||
|
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
|
||||||
|
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
|
||||||
|
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWingManualSections::BestPerformance()
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT(
|
||||||
|
"\n BEST PERFORMANCE:"
|
||||||
|
"\n"
|
||||||
|
"\n UE Wingman is much faster than the LLM. Therefore, it is"
|
||||||
|
"\n advantageous to batch: chain multiple ue-wingman commands"
|
||||||
|
"\n together using bash semicolon."
|
||||||
|
"\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWingManualSections::ImportantCommands()
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT(
|
||||||
|
"\n IMPORTANT COMMANDS:"
|
||||||
|
"\n"
|
||||||
|
"\n Documentation_Manual: print the entire manual"
|
||||||
|
"\n Documentation_Section: print a single section of the manual"
|
||||||
|
"\n Documentation_Commands: print concise list of all ue-wingman commands"
|
||||||
|
"\n Documentation_Command: detailed documentation for a single ue-wingman command"
|
||||||
|
"\n Documentation_CreateAssets: list of commands that create new assets"
|
||||||
"\n Blueprint_Dump: a summary of any blueprint"
|
"\n Blueprint_Dump: a summary of any blueprint"
|
||||||
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
"\n Graph_Dump: a fairly detailed listing of any Graph"
|
||||||
"\n Details_Dump: Dump the details panel for a given object"
|
"\n Details_Dump: Dump the details panel for a given object"
|
||||||
"\n Details_Set: Manipulate the details panel for a given object"
|
"\n Details_Set: Manipulate the details panel for a given object"
|
||||||
"\n"
|
"\n"
|
||||||
"\n You can use Documentation_Commands(Query=SomeCommand,Verbose=true)"
|
|
||||||
"\n to get detailed help for a specific command."
|
|
||||||
"\n"
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Abridged)
|
TSet<FName> WingManual::GetSections()
|
||||||
|
{
|
||||||
|
TSet<FName> Result;
|
||||||
|
for (TFieldIterator<UFunction> It(UWingManualSections::StaticClass(), EFieldIterationFlags::None); It; ++It)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("\nUse command 'Documentation_Manual' to see the full manual.\n"));
|
Result.Add(It->GetFName());
|
||||||
}
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
|
||||||
|
{
|
||||||
|
if (Sections.IsEmpty()) return;
|
||||||
|
if (Prefix) Output.Print(Prefix);
|
||||||
|
bool bFirst = true;
|
||||||
|
for (const FName& Section : Sections)
|
||||||
|
{
|
||||||
|
if (!bFirst) WingOut::Stdout.Print(TEXT(", "));
|
||||||
|
bFirst = false;
|
||||||
|
Output.Printf(TEXT("%s"), *Section.ToString());
|
||||||
|
}
|
||||||
|
if (Prefix) Output.Print(TEXT("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WingManual::PrintSection(FName Section)
|
||||||
|
{
|
||||||
|
UFunction* Func = UWingManualSections::StaticClass()->FindFunctionByName(Section);
|
||||||
|
if (!Func) return false;
|
||||||
|
UWingManualSections::StaticClass()->GetDefaultObject()->ProcessEvent(Func, nullptr);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verbose)
|
void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verbose)
|
||||||
@@ -300,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)
|
||||||
@@ -322,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);
|
||||||
@@ -341,7 +290,11 @@ FString FWingProperty::GetText() const
|
|||||||
return TEXT("None");
|
return TEXT("None");
|
||||||
}
|
}
|
||||||
FString Result;
|
FString Result;
|
||||||
Prop->ExportTextItem_InContainer(Result, Container, nullptr, nullptr, PPF_None);
|
// DefaultValue == PropertyValue makes ExportText_Direct's Data==Delta
|
||||||
|
// short-circuit fire for every subfield, so default-valued fields still
|
||||||
|
// get emitted (e.g. SizeRule=Automatic on FSlateChildSize).
|
||||||
|
const void* Value = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||||
|
Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, nullptr, PPF_None);
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,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;
|
||||||
}
|
}
|
||||||
@@ -432,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
|
||||||
@@ -461,7 +412,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FWingProperty> Result = GetVisible(Obj);
|
TArray<FWingProperty> Result = GetVisible(Obj, Mutable);
|
||||||
|
|
||||||
// If it's a Material Graph node, also collect properties from
|
// If it's a Material Graph node, also collect properties from
|
||||||
// the associated material expression.
|
// the associated material expression.
|
||||||
@@ -470,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,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)
|
||||||
|
{
|
||||||
|
FWingProperty& P = Props[PropIndex];
|
||||||
|
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
|
||||||
|
const bool bIsRest =
|
||||||
|
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
|
||||||
|
|
||||||
// Build a set of known property names for the unknown-field check.
|
if (bIsRest)
|
||||||
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);
|
if (PropIndex + 1 != Props.Num())
|
||||||
if (!KnownKeys.Contains(Name))
|
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
|
||||||
Ok = false;
|
*WingUtils::FormatName(P.Prop));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate each property from JSON
|
|
||||||
for (FWingProperty& P : Props)
|
|
||||||
{
|
|
||||||
FString JsonKey = WingUtils::FormatName(P.Prop);
|
|
||||||
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
|
|
||||||
if (!Value)
|
|
||||||
{
|
|
||||||
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
|
|
||||||
if (!Optional)
|
|
||||||
{
|
|
||||||
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
|
|
||||||
Ok = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!P.SetJson(*Value, Errors)) Ok = false;
|
|
||||||
}
|
|
||||||
return Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
|
|
||||||
{
|
|
||||||
// Make sure they passed in a JSON object.
|
|
||||||
TSharedPtr<FJsonObject> Obj = Json.AsObject();
|
|
||||||
if (Obj == nullptr)
|
|
||||||
{
|
|
||||||
Errors.Printf(TEXT("property data should be stored in a json object\n"));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
|
|
||||||
|
FWingRestOfArgv Rest;
|
||||||
|
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
|
||||||
|
{
|
||||||
|
Rest.Argv.Add(Argv[I]);
|
||||||
|
}
|
||||||
|
P.Prop->SetValue_InContainer(P.Container, &Rest);
|
||||||
|
ArgIndex = Argv.Num();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArgIndex >= Argv.Num())
|
||||||
|
{
|
||||||
|
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
|
||||||
|
*WingUtils::FormatName(P.Prop));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
|
||||||
|
ArgIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArgIndex < Argv.Num())
|
||||||
|
{
|
||||||
|
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
|
||||||
|
*Argv[ArgIndex]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -576,11 +524,6 @@ bool FWingProperty::CheckImportTextResult(const FString &Value, WingOut Errors)
|
|||||||
return true;
|
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,16 +169,35 @@ 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)
|
||||||
|
{
|
||||||
|
TArray<FString> Argv;
|
||||||
|
FString ResponseText;
|
||||||
|
|
||||||
|
if (DeserializeArgv(RequestBytes, Argv))
|
||||||
|
{
|
||||||
|
PreCallHandler();
|
||||||
|
TryCallHandler(Argv);
|
||||||
|
ResponseText = PostCallHandler();
|
||||||
|
}
|
||||||
|
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
|
||||||
|
|
||||||
|
FTCHARToUTF8 Utf8(*ResponseText);
|
||||||
|
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UWingServer::PreCallHandler()
|
||||||
{
|
{
|
||||||
LogCapture.CapturedErrors.Empty();
|
LogCapture.CapturedErrors.Empty();
|
||||||
LogCapture.bEnabled = true;
|
LogCapture.bEnabled = true;
|
||||||
WingOut::StdoutBuffer.Reset();
|
WingOut::StdoutBuffer.Reset();
|
||||||
SuggestedManualSections.Empty();
|
SuggestedManualSections.Empty();
|
||||||
|
bSuggestHandlerHelp = false;
|
||||||
LastHandler = nullptr;
|
LastHandler = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
TryCallHandler(Line);
|
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)
|
||||||
@@ -186,48 +205,46 @@ FString UWingServer::HandleRequest(const FString& Line)
|
|||||||
WingOut::Stdout.Printf(TEXT("UE_LOG: %s\n"), *Msg);
|
WingOut::Stdout.Printf(TEXT("UE_LOG: %s\n"), *Msg);
|
||||||
}
|
}
|
||||||
LogCapture.CapturedErrors.Empty();
|
LogCapture.CapturedErrors.Empty();
|
||||||
|
if (bSuggestHandlerHelp || (!SuggestedManualSections.IsEmpty()))
|
||||||
|
{
|
||||||
|
if (LastHandler) WingManual::PrintHandlerHelp(*LastHandler);
|
||||||
|
if ((LastHandler == nullptr) || (LastHandler->Name != TEXT("Documentation_Manual")))
|
||||||
|
{
|
||||||
|
WingOut::Stdout.Print(TEXT("To see manual: command=Documentation_Manual\n"));
|
||||||
|
}
|
||||||
if (!SuggestedManualSections.IsEmpty())
|
if (!SuggestedManualSections.IsEmpty())
|
||||||
{
|
{
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
WingManual::PrintSectionNames(TEXT("Suggested manual sections: "),
|
||||||
WingManual::PrintManual(SuggestedManualSections, LastHandler, true);
|
SuggestedManualSections, WingOut::Stdout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FString Result = WingOut::StdoutBuffer.ToString();
|
FString Result = WingOut::StdoutBuffer.ToString();
|
||||||
WingOut::StdoutBuffer.Reset();
|
WingOut::StdoutBuffer.Reset();
|
||||||
for (int32 i = 0; i < Result.Len(); ++i)
|
|
||||||
{
|
|
||||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
|
||||||
}
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UWingServer::TryCallHandler(const FString &Line)
|
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
|
||||||
{
|
{
|
||||||
// Turn the request string into a JSON tree.
|
FString Command = "Documentation_Manual";
|
||||||
TSharedPtr<FJsonObject> Request;
|
if (Argv.Num() > 0)
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
|
|
||||||
FJsonSerializer::Deserialize(Reader, Request);
|
|
||||||
if (!Request.IsValid())
|
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
|
Command = Argv[0];
|
||||||
return;
|
Argv = Argv.RightChop(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the command from the request.
|
if ((Command.Equals(TEXT("--help"))) ||
|
||||||
FString Command;
|
(Command.Equals(TEXT("-help"))) ||
|
||||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
(Command.Equals(TEXT("help"))))
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
|
Command = "Documentation_Manual";
|
||||||
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
Request->RemoveField(TEXT("command"));
|
|
||||||
|
|
||||||
// Find the handler for the specified command.
|
// Find the handler for the specified command.
|
||||||
FWingHandlerConfig* Found = FindHandler(Command);
|
FWingHandlerConfig* Found = FindHandler(Command);
|
||||||
if (!Found)
|
if (!Found)
|
||||||
{
|
{
|
||||||
WingOut::Stdout.Printf(TEXT("Unknown command: %s"), *Command);
|
WingOut::Stdout.Printf(TEXT("Unknown command: %s\n"), *Command);
|
||||||
UWingServer::SuggestManual(WingManual::Section::ImportantCommands);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, ImportantCommands));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LastHandler = Found;
|
LastHandler = Found;
|
||||||
@@ -237,11 +254,11 @@ void UWingServer::TryCallHandler(const FString &Line)
|
|||||||
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::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,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;
|
|
||||||
|
TArray<uint8> Response;
|
||||||
if (!ProcessRequestOnGameThread(Request, Response))
|
if (!ProcessRequestOnGameThread(Request, Response))
|
||||||
{
|
{
|
||||||
Client->bDone = true;
|
Client->bDone = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the response back, null-terminated (blocking)
|
SendAll(Socket, Response.GetData(), Response.Num());
|
||||||
FTCHARToUTF8 Utf8(*Response);
|
|
||||||
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
|
|
||||||
Utf8.Length() + 1))
|
|
||||||
{
|
|
||||||
Client->bDone = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Client->bDone = true;
|
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())
|
||||||
{
|
{
|
||||||
|
if (RequestBytes.Num() - Offset < 4)
|
||||||
|
{
|
||||||
|
Argv.Empty();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 MessageLen =
|
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
|
||||||
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
|
Offset += 4;
|
||||||
OutRequest = FString::ConstructFromPtrSize(
|
|
||||||
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
|
if ((uint32)(RequestBytes.Num() - Offset) < Length)
|
||||||
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
|
|
||||||
if (RemainingBytes > 0)
|
|
||||||
{
|
{
|
||||||
FMemory::Memmove(
|
Argv.Empty();
|
||||||
RecvBuf.GetData(),
|
return false;
|
||||||
RecvBuf.GetData() + MessageLen + 1,
|
|
||||||
RemainingBytes);
|
|
||||||
}
|
}
|
||||||
RecvLen = RemainingBytes;
|
|
||||||
|
Argv.Add(FString::ConstructFromPtrSize(
|
||||||
|
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
|
||||||
|
Length));
|
||||||
|
Offset += (int32)Length;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
{
|
|
||||||
if (RecvBuf.Num() >= MaxRecvBufBytes)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
|
|
||||||
UnusedSpace = RecvBuf.Num() - RecvLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Unreal's FSocket API is fundamentally broken: recv cannot
|
||||||
|
// differentiate between a socket that has been cleanly closed
|
||||||
|
// and a socket that has had an error. So we have no choice
|
||||||
|
// but to just read until recv returns false (which could be a
|
||||||
|
// clean close or an error). Then, we check if we have a cleanly
|
||||||
|
// encoded payload: if so, we assume everything is fine.
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
uint8 Temp[ChunkSize];
|
||||||
int32 BytesRead = 0;
|
int32 BytesRead = 0;
|
||||||
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
|
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (BytesRead <= 0) break;
|
||||||
|
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (BytesRead <= 0)
|
RecvBuf.Append(Temp, BytesRead);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RecvLen += BytesRead;
|
if (RecvBuf.Num() < 4) return false;
|
||||||
|
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
|
||||||
|
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
|
||||||
|
RecvBuf.RemoveAt(0, 4);
|
||||||
|
|
||||||
|
OutRequest = MoveTemp(RecvBuf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,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);
|
||||||
@@ -475,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; });
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ void UWingTypes::PrintParseError(WingTokenizer& Tok, const TCHAR* Message, WingO
|
|||||||
{
|
{
|
||||||
FString TypeText(Tok.GetRange(NAME_StartOfType, 1));
|
FString TypeText(Tok.GetRange(NAME_StartOfType, 1));
|
||||||
Errors.Printf(TEXT("ERROR parsing type '%s' — %s\n"), *TypeText, Message);
|
Errors.Printf(TEXT("ERROR parsing type '%s' — %s\n"), *TypeText, Message);
|
||||||
UWingServer::SuggestManual(WingManual::Section::Types);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, ExpressingTypes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
if (IsNone && OutPinType.IsContainer())
|
if (IsNone && OutPinType.IsContainer())
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: 'None' is not allowed in an array/set/map\n"));
|
Errors.Printf(TEXT("ERROR: 'None' is not allowed in an array/set/map\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +634,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
if (!Require.AllowNone && IsNone)
|
if (!Require.AllowNone && IsNone)
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: 'None' is not allowed here\n"));
|
Errors.Printf(TEXT("ERROR: 'None' is not allowed here\n"));
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +644,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
{
|
{
|
||||||
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
||||||
Errors.Printf(TEXT("ERROR: Type '%s' is a container, not allowed here\n"), *Text);
|
Errors.Printf(TEXT("ERROR: Type '%s' is a container, not allowed here\n"), *Text);
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,7 +655,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Need a type which is an %s, got a %s instead.\n"),
|
Errors.Printf(TEXT("ERROR: Need a type which is an %s, got a %s instead.\n"),
|
||||||
*Require.PinCategory.ToString(), *OutPinType.PinCategory.ToString());
|
*Require.PinCategory.ToString(), *OutPinType.PinCategory.ToString());
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -668,13 +668,13 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
{
|
{
|
||||||
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
||||||
Errors.Printf(TEXT("ERROR: '%s' is an interface, not a class\n"), *Text);
|
Errors.Printf(TEXT("ERROR: '%s' is an interface, not a class\n"), *Text);
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
if (!IsChildOf(OutPinType, Require.IsChildOf))
|
if (!IsChildOf(OutPinType, Require.IsChildOf))
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: Type must derive from %s\n"), *WingUtils::FormatName(Require.IsChildOf));
|
Errors.Printf(TEXT("ERROR: Type must derive from %s\n"), *WingUtils::FormatName(Require.IsChildOf));
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +685,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
{
|
{
|
||||||
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
||||||
Errors.Printf(TEXT("ERROR: Not a blueprint type: %s\n"), *Text);
|
Errors.Printf(TEXT("ERROR: Not a blueprint type: %s\n"), *Text);
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,7 +696,7 @@ bool UWingTypes::TextToType(WingTokenizer &Tok, FEdGraphPinType& OutPinType, con
|
|||||||
{
|
{
|
||||||
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
FString Text(Tok.GetRange(NAME_StartOfType, 0));
|
||||||
Errors.Printf(TEXT("ERROR: Not a blueprintable type: %s\n"), *Text);
|
Errors.Printf(TEXT("ERROR: Not a blueprintable type: %s\n"), *Text);
|
||||||
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
|
UWingServer::SuggestHandlerHelp();
|
||||||
OutPinType = FEdGraphPinType(); return false;
|
OutPinType = FEdGraphPinType(); return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ FName WingUtils::CheckInternalizeID(const FString &ExternalID, WingOut Errors)
|
|||||||
if (!Error.IsEmpty())
|
if (!Error.IsEmpty())
|
||||||
{
|
{
|
||||||
Errors.Printf(TEXT("%s\n"), *Error);
|
Errors.Printf(TEXT("%s\n"), *Error);
|
||||||
UWingServer::SuggestManual(WingManual::Section::EscapeSequences);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, EscapeSequencesInFNames));
|
||||||
}
|
}
|
||||||
return InternalID;
|
return InternalID;
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ FName WingUtils::CheckProposedName(const FString &ExternalID, const TSet<FName>
|
|||||||
{
|
{
|
||||||
Errors.Printf(TEXT("ERROR: id %s would not be a readable id, may not create item with this name"),
|
Errors.Printf(TEXT("ERROR: id %s would not be a readable id, may not create item with this name"),
|
||||||
*ExternalID);
|
*ExternalID);
|
||||||
UWingServer::SuggestManual(WingManual::Section::EscapeSequences);
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, EscapeSequencesInFNames));
|
||||||
return FName();
|
return FName();
|
||||||
}
|
}
|
||||||
if (InUse.Contains(InternalID))
|
if (InUse.Contains(InternalID))
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -225,10 +179,11 @@ public:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UObject* Object;
|
UObject* Object;
|
||||||
|
|
||||||
|
void *StructBase;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
UStruct* StructType;
|
UStruct* StructType;
|
||||||
|
|
||||||
void *StructBase;
|
|
||||||
|
|
||||||
bool Editable;
|
bool Editable;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "WingBasics.h"
|
||||||
|
#include "WingManual.generated.h"
|
||||||
|
|
||||||
struct FWingHandlerConfig;
|
struct FWingHandlerConfig;
|
||||||
enum class EWingHandlerKind;
|
enum class EWingHandlerKind;
|
||||||
@@ -7,23 +9,46 @@ enum class EWingHandlerKind;
|
|||||||
class WingManual
|
class WingManual
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class Section
|
|
||||||
{
|
|
||||||
All,
|
|
||||||
HandlerHelp,
|
|
||||||
Paths,
|
|
||||||
Types,
|
|
||||||
VariableDeclarations,
|
|
||||||
EscapeSequences,
|
|
||||||
Whitespace,
|
|
||||||
MaterialEditing,
|
|
||||||
ImportantCommands,
|
|
||||||
};
|
|
||||||
|
|
||||||
static void PrintHandlerPrototype(const FWingHandlerConfig& Handler);
|
static void PrintHandlerPrototype(const FWingHandlerConfig& Handler);
|
||||||
static void PrintHandlerArguments(const FWingHandlerConfig& Handler);
|
static void PrintHandlerArguments(const FWingHandlerConfig& Handler);
|
||||||
static void PrintHandlerDescription(const FWingHandlerConfig& Handler);
|
static void PrintHandlerDescription(const FWingHandlerConfig& Handler);
|
||||||
static void PrintHandlerHelp(const FWingHandlerConfig& Handler);
|
static void PrintHandlerHelp(const FWingHandlerConfig& Handler);
|
||||||
static void PrintManual(TSet<Section> Sections, const FWingHandlerConfig* Handler, bool Abridged);
|
|
||||||
static void Commands(EWingHandlerKind Kind, const FString& Query, bool Verbose);
|
static void Commands(EWingHandlerKind Kind, const FString& Query, bool Verbose);
|
||||||
|
static TSet<FName> GetSections();
|
||||||
|
static bool PrintSection(FName Section);
|
||||||
|
static void PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output);
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UWingManualSections : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION()
|
||||||
|
static void FetcherPaths();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void ExpressingTypes();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void VariableDeclarations();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void EscapeSequencesInFNames();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void MaterialEditing();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void ImportantCommands();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
static void NodeContextMenus();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
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;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "WingServer.generated.h"
|
#include "WingServer.generated.h"
|
||||||
|
|
||||||
class FSocket;
|
class FSocket;
|
||||||
|
class FJsonObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
|
||||||
@@ -48,7 +49,10 @@ public:
|
|||||||
static void AddTouchedObject(UObject* Obj) { GWingServer->Notifier.AddTouchedObject(Obj); }
|
static void AddTouchedObject(UObject* Obj) { GWingServer->Notifier.AddTouchedObject(Obj); }
|
||||||
|
|
||||||
/** Suggest that a manual section be printed after the handler finishes. */
|
/** Suggest that a manual section be printed after the handler finishes. */
|
||||||
static void SuggestManual(WingManual::Section Section) { GWingServer->SuggestedManualSections.Add(Section); }
|
static void SuggestManual(FName Section) { GWingServer->SuggestedManualSections.Add(Section); }
|
||||||
|
|
||||||
|
/** Suggest that handler help be printed after the handler finishes. */
|
||||||
|
static void SuggestHandlerHelp() { GWingServer->bSuggestHandlerHelp = true; }
|
||||||
|
|
||||||
/** Port the server is listening on. */
|
/** Port the server is listening on. */
|
||||||
int32 GetPort() const { return Port; }
|
int32 GetPort() const { return Port; }
|
||||||
@@ -64,17 +68,19 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FWingNotifier Notifier;
|
FWingNotifier Notifier;
|
||||||
FWingHandlerConfig* LastHandler;
|
FWingHandlerConfig* LastHandler;
|
||||||
TSet<WingManual::Section> SuggestedManualSections;
|
bool bSuggestHandlerHelp;
|
||||||
|
TSet<FName> SuggestedManualSections;
|
||||||
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);
|
||||||
void TryCallHandler(const FString &Line);
|
void PreCallHandler();
|
||||||
|
FString PostCallHandler();
|
||||||
|
void TryCallHandler(TArrayView<const FString> Argv);
|
||||||
|
|
||||||
// ----- TCP server -----
|
// ----- TCP server -----
|
||||||
FSocket* ListenSocket = nullptr;
|
FSocket* ListenSocket = nullptr;
|
||||||
@@ -91,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,16 +75,6 @@ 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
|
||||||
@@ -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,162 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
MCP stdio-to-TCP bridge for UE Wingman.
|
|
||||||
|
|
||||||
Exposes a single MCP tool "unreal" that forwards JSON commands to the
|
|
||||||
UE Wingman TCP server in the Unreal Editor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import socket
|
|
||||||
|
|
||||||
HOST = "localhost"
|
|
||||||
PORT = 9851
|
|
||||||
CONNECT_TIMEOUT = 2
|
|
||||||
READ_TIMEOUT = 120
|
|
||||||
|
|
||||||
TOOL_DESCRIPTION = (
|
|
||||||
"Send a command to the Unreal Editor's UE Wingman plugin. "
|
|
||||||
"The 'command' field specifies which operation to perform; "
|
|
||||||
"additional fields are command-specific parameters. "
|
|
||||||
'Use {"command": "Documentation_Manual"} to get an overview. '
|
|
||||||
"If the editor is not running, the call will return an error; "
|
|
||||||
"just ask the user to start the editor and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
TOOL_SCHEMA = {
|
|
||||||
"name": "unreal",
|
|
||||||
"description": TOOL_DESCRIPTION,
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {"type": "string", "description": "The command to execute"},
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
"additionalProperties": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
|
||||||
"""Try to connect to the editor. Returns True on success."""
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(CONNECT_TIMEOUT)
|
|
||||||
s.connect((HOST, PORT))
|
|
||||||
s.settimeout(READ_TIMEOUT)
|
|
||||||
sock = s
|
|
||||||
return True
|
|
||||||
except (ConnectionRefusedError, socket.timeout, OSError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect():
|
|
||||||
global sock
|
|
||||||
if sock is not None:
|
|
||||||
try:
|
|
||||||
sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
|
|
||||||
def send_and_receive(message):
|
|
||||||
"""Send a JSON message to the editor and return the null-terminated response."""
|
|
||||||
data = json.dumps(message) + "\0"
|
|
||||||
sock.sendall(data.encode())
|
|
||||||
|
|
||||||
result = b""
|
|
||||||
while True:
|
|
||||||
chunk = sock.recv(65536)
|
|
||||||
if not chunk:
|
|
||||||
raise ConnectionError("Connection closed")
|
|
||||||
result += chunk
|
|
||||||
if b"\0" in result:
|
|
||||||
break
|
|
||||||
|
|
||||||
return result[:result.index(b"\0")].decode()
|
|
||||||
|
|
||||||
|
|
||||||
def forward_to_editor(arguments):
|
|
||||||
"""Forward arguments to the editor, return the result dict."""
|
|
||||||
if not connect():
|
|
||||||
return {"error": "Unreal Editor is not running. Start the editor and try again."}
|
|
||||||
try:
|
|
||||||
return send_and_receive(arguments)
|
|
||||||
except Exception:
|
|
||||||
disconnect()
|
|
||||||
# Retry once in case the connection was stale
|
|
||||||
if connect():
|
|
||||||
try:
|
|
||||||
return send_and_receive(arguments)
|
|
||||||
except Exception:
|
|
||||||
disconnect()
|
|
||||||
return {"error": "Lost connection to Unreal Editor."}
|
|
||||||
|
|
||||||
|
|
||||||
def make_jsonrpc(msg_id, result):
|
|
||||||
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_message(msg):
|
|
||||||
"""Handle one JSON-RPC message from Claude Code."""
|
|
||||||
msg_id = msg.get("id")
|
|
||||||
method = msg.get("method", "")
|
|
||||||
|
|
||||||
# Notifications don't get responses
|
|
||||||
if msg_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if method == "initialize":
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"protocolVersion": "2024-11-05",
|
|
||||||
"capabilities": {"tools": {}},
|
|
||||||
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
|
|
||||||
})
|
|
||||||
|
|
||||||
if method == "tools/list":
|
|
||||||
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
|
|
||||||
|
|
||||||
if method == "tools/call":
|
|
||||||
params = msg.get("params", {})
|
|
||||||
arguments = params.get("arguments", {})
|
|
||||||
result = forward_to_editor(arguments)
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
text = result["error"]
|
|
||||||
else:
|
|
||||||
text = result
|
|
||||||
return make_jsonrpc(msg_id, {
|
|
||||||
"content": [{"type": "text", "text": text}],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": msg_id,
|
|
||||||
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
for line in sys.stdin:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
msg = json.loads(line)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
response = handle_message(msg)
|
|
||||||
if response is not None:
|
|
||||||
sys.stdout.write(json.dumps(response) + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,37 +1,23 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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,17 +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)
|
|
||||||
print(json.dumps(parsed, indent=2))
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print(result)
|
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user