Compare commits

..

57 Commits

Author SHA1 Message Date
d737879ed6 More slash command stuff 2026-05-26 18:42:48 -04:00
46051526e6 New slash command parser 2026-05-26 18:20:35 -04:00
8dab0d16b7 CPL command 2026-05-26 15:07:42 -04:00
933c1ac6c3 More tweaks on tracebacks 2026-05-21 19:40:30 -04:00
521d4726ad Make lua tracebacks a little better 2026-05-21 19:24:11 -04:00
2bfa3024f1 Lots of work on the lua read-eval-print loop 2026-05-21 18:41:09 -04:00
f7983b1f02 Final form of radial menus 2026-05-19 02:55:31 -04:00
36ec4a3b9b Add new input device processor 2026-05-18 19:26:11 -04:00
1c2be1b4d8 Radial menus are in good shape 2026-05-18 16:12:26 -04:00
0b23c82e73 Progress on radial menus 2026-05-18 02:26:28 -04:00
b1defd821b More work on radial menus 2026-05-16 02:27:18 -04:00
e17f5417f2 Real progress on radial menus 2026-05-16 01:49:26 -04:00
c0848c2670 Completing downgrade to 5.5.4 2026-05-15 19:37:34 -04:00
94e6385f14 Downgrading to Unreal 5.5.4 2026-05-15 18:28:41 -04:00
1328f6e5f7 Working on radial menus 2026-05-15 18:14:38 -04:00
5d2377df1d More work on the argv conversion of ue-wingman 2026-05-13 22:03:19 -04:00
e0d45cc1db Refactoring ue-wingman to be a command-line only tool 2026-05-13 21:36:40 -04:00
ff9c045c8e Initial code for radial rendering 2026-05-11 00:13:30 -04:00
e669140e2c Layout code for radial menu complete. 2026-05-09 03:16:41 -04:00
420ea088d7 More work on radial menus 2026-05-08 17:55:11 -04:00
b00ec49e91 A few tweaks for the latest engine version 2026-05-08 14:16:52 -04:00
1aa888ac82 THinking about radial menus 2026-05-08 04:26:54 -04:00
236693fca6 More work on Prompt Widget 2026-05-07 04:11:37 -04:00
b5e121f884 Update to latest engine 2026-05-05 19:12:03 -04:00
ac4302141c Add gitdeps cache 2026-05-05 18:19:30 -04:00
e16e0978b0 Upgrade to unreal 5.7.4 2026-05-05 16:26:49 -04:00
3be98f3617 Yet another fix for UEDataFormatter.py 2026-05-04 16:51:39 -04:00
3cf984ff65 Fewer log messages from UE Wingman 2026-05-04 16:11:06 -04:00
78c85660c9 Prompt widget is pretty good now 2026-05-04 15:52:05 -04:00
3e7e6a2ae4 More progress on prompt 2026-05-04 15:27:28 -04:00
9d37f02d44 More work on prompt widget 2026-05-04 11:55:08 -04:00
97b5a3c593 Initial work on prompt widget 2026-05-04 02:14:14 -04:00
ae0defbad9 WingProperty now contains an object pointer. This made it possible to implement structprop a lot better, it now can dig into objects it couldn't dig into before, including structprop-within-structprop 2026-04-27 19:06:57 -04:00
9598004e6d remove __intellisense__ 2026-04-27 04:15:33 -04:00
4680a0f3f4 Crosshair is back 2026-04-25 01:14:16 -04:00
3f6ef4b56c Simplify keyboard focus rule to just 'widget in front', full stop. 2026-04-24 20:17:08 -04:00
960abba07f Intellisense clangd fix 2026-04-22 23:32:50 -04:00
a689d59ea0 More work on focus, and good docs 2026-04-22 22:52:04 -04:00
a964211cc8 Fix some issues with new root canvas stuff 2026-04-22 17:02:24 -04:00
d985a6bc55 More fixing everything 2026-04-22 07:22:55 -04:00
4420c52b74 Working on new root canvas stuff 2026-04-21 22:28:22 -04:00
8e5d43fd24 Working on new root canvas stuff 2026-04-21 21:26:06 -04:00
ec983951fe InputModeRequests now use less-than operator to sort 2026-04-21 04:24:35 -04:00
9787522ef6 UE Data formatters done. 2026-04-21 01:00:45 -04:00
0d607ba277 More work on data formatters 2026-04-20 20:52:17 -04:00
4c1eebab96 Working on lldb data formatting some more 2026-04-20 08:42:36 -04:00
f3e1daf4fe More work on getting lldb data formatters in better shape. 2026-04-20 08:23:36 -04:00
21d8c40005 Better data formatters in progress. 2026-04-20 05:42:34 -04:00
275698c5aa Trying to improve lldb 2026-04-19 05:03:11 -04:00
dabb5b8f0b Lots of work on focus management 2026-04-19 03:01:23 -04:00
fd970f20c3 Lots of work on input processing 2026-04-18 01:11:21 -04:00
6388de9b39 Much work on input mode switching 2026-04-17 23:43:28 -04:00
7a09da8a4e Player controller code to sort input components better: widgets with 'Stop Input' go to top of priority stack, and also, priority actually works now. 2026-04-17 17:56:10 -04:00
d396f394ab Sequences are now implemented in UE Wingman 2026-04-17 06:46:18 -04:00
f19e8ccb72 Working on implementing batch commands for UE wingman 2026-04-17 05:51:13 -04:00
6b057d1514 Improve docs for UlxUserWidget 2026-04-17 03:17:15 -04:00
392faff205 Clean up the player controller 2026-04-17 02:53:32 -04:00
124 changed files with 7611 additions and 1930 deletions

2
.gitignore vendored
View File

@@ -27,6 +27,7 @@ UnrealEngine
*.vcproj
.ignore
.gitdeps-cache/**
.vscode/**
Config/**
Saved/**
@@ -52,3 +53,4 @@ GPF-output/**
__pycache__/
.clangd-query/
COMMIT.txt
CLAUDE.md

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"ue-wingman": {
"command": "python3",
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
}
}
}

View File

@@ -25,9 +25,16 @@
- `Docs/` — Documentation.
- `Config/` — Unreal config files
- `EnginePatches/` — Custom engine modifications
- `Plugins/UEWingman/' - An MCP that gives you control over the unreal editor.
- `Plugins/UEWingman/` - A plugin that gives you control over the unreal editor.
- `../integration.UE/` - the unreal engine source tree
## Using ue-wingman
- Drive it from bash using: ue-wingman <Command> <Arg1> <Arg2> ...
- ue-wingman Documentation_Manual
- ue-wingman Documentation_Commands
- ue-wingman Documentation_Command <specific_command>
## Coding Conventions
- Prefer early returns and `continue` to reduce nesting (never-nester style).

1498
Art/gamepad.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 65 KiB

321
Art/radial.svg Normal file
View 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
View 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
View 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

View File

@@ -4,7 +4,7 @@
[/Script/Engine.Engine]
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient
GameViewportClientClassName=/Script/Integration.lxViewportClient
[/Script/EngineSettings.GameMapsSettings]
GameDefaultMap=/Game/LpxLevel.LpxLevel

View File

@@ -84,3 +84,16 @@ DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.Defaul
-ConsoleKeys=Tilde
+ConsoleKeys=Tilde
[InputPlatformSettings_Linux InputPlatformSettings]
MaxPlatformUserCount=8
input.DeviceMappingPolicy=1
MaxTriggerFeedbackPosition=8
MaxTriggerFeedbackStrength=8
MaxTriggerVibrationTriggerPosition=9
MaxTriggerVibrationFrequency=255
MaxTriggerVibrationAmplitude=8
+HardwareDevices=(InputClassName="",HardwareDeviceIdentifier="",PrimaryDeviceType=Unspecified,SupportedFeaturesMask=0)
+HardwareDevices=(InputClassName="DefaultKeyboardAndMouse",HardwareDeviceIdentifier="KBM",PrimaryDeviceType=KeyboardAndMouse,SupportedFeaturesMask=3)
+HardwareDevices=(InputClassName="DefaultGamepad",HardwareDeviceIdentifier="Gamepad",PrimaryDeviceType=Gamepad,SupportedFeaturesMask=4)
+HardwareDevices=(InputClassName="DefaultMobileTouch",HardwareDeviceIdentifier="MobileTouch",PrimaryDeviceType=Touch,SupportedFeaturesMask=8)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Luprex/lxPrompts.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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/teardrop.uasset LFS Normal file

Binary file not shown.

BIN
Content/Widgets/white-dot.uasset LFS Normal file

Binary file not shown.

View File

@@ -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;
};

View File

@@ -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.

View 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.

View 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
View 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.

View File

@@ -1,19 +1,12 @@
* UE Wingman Function Overrides.
* Keyboard Event Handling
* Menus
* Add a slash-command to reload lua source code.
* Skeletal Mesh Tangible
* Implement Interactive Temporary Variables
* A better text console
* Get rid of 3x3 Gridpanel stuff
* Object-Oriented Lua Support

View File

@@ -1,26 +1,3 @@
--- Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py.orig 2026-04-12 22:58:33.989318455 -0400
+++ Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py 2025-11-10 23:34:18.481538118 -0500
@@ -32,7 +32,7 @@
if DataVal == 0:
Val = 'NULL'
else:
- Expr = '(char16_t*)(%s)' % Data
+ Expr = '(char16_t*)(%s)' % DataVal
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
elif Type.IsReferenceType():
@@ -47,6 +47,11 @@
Expr = '(char16_t*)(%s)' % valobj.GetAddress()
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
+ else:
+ DataVal = valobj.GetValueAsUnsigned(0)
+ Expr = '(char16_t)(%s)' % DataVal
+ ValRef = valobj.CreateValueFromExpression('string', Expr)
+ Val = ValRef.GetSummary()
return Val
def UESignedCharSummaryProvider(valobj,dict):
--- Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp.orig 2026-04-12 22:58:34.075320964 -0400
+++ Engine/Plugins/Developer/VisualStudioCodeSourceCodeAccess/Source/VisualStudioCodeSourceCodeAccess/Private/VisualStudioCodeSourceCodeAccessor.cpp 2026-04-12 23:03:40.139226303 -0400
@@ -149,7 +149,7 @@

View 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);

View 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)
{
```

View File

@@ -28,7 +28,8 @@
"files.watcherExclude": {
"[UNREALENGINE]/Engine/**": true,
"[UNREALENGINE]/Samples/**": true,
"[UNREALENGINE]/Templates/**": true
"[UNREALENGINE]/Templates/**": true,
"**/.*": true
},
"files.associations": {
"**/include/**": "cpp",
@@ -40,7 +41,7 @@
},
"editor.acceptSuggestionOnEnter": "off",
"C_Cpp.intelliSenseEngine": "disabled",
"clangd.path": "/usr/bin/clangd-15",
"clangd.path": "/usr/bin/clangd-16",
"clangd.arguments": [
"--log=verbose",
"--query-driver=/usr/bin/g++",
@@ -50,6 +51,9 @@
],
"C_Cpp.autocomplete": "disabled",
"search.useIgnoreFiles": true,
"files.readonlyInclude": {
"[UNREALENGINE]/**": true
},
"search.exclude": {
"**/Intermediate": true,
"**/Saved": true,
@@ -123,10 +127,11 @@
"type": "lldb",
"console": "integratedTerminal",
"initCommands": [
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
"settings set target.inline-breakpoint-strategy always",
"settings set target.prefer-dynamic-value no-run-target",
"process handle SIGTRAP --notify false --pass false --stop false",
"target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\""
"target stop-hook add --one-liner \"p FUnixPlatformMisc::UngrabAllInput()\""
]
}
},
@@ -146,8 +151,10 @@
"type": "lldb",
"console": "integratedTerminal",
"initCommands": [
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py",
"settings set target.inline-breakpoint-strategy always"
"command script import [INTEGRATION]/tools/UEDataFormatter.py",
"settings set target.inline-breakpoint-strategy always",
"settings set target.prefer-dynamic-value no-run-target",
"process handle SIGTRAP --notify false --pass false --stop false"
]
},
{
@@ -160,7 +167,9 @@
"type": "lldb",
"console": "integratedTerminal",
"initCommands": [
"settings set target.inline-breakpoint-strategy always"
"settings set target.inline-breakpoint-strategy always",
"settings set target.prefer-dynamic-value no-run-target",
"process handle SIGTRAP --notify false --pass false --stop false"
]
}
]

View File

@@ -16,14 +16,12 @@ blueprints, widget blueprints, and materials.
## How Does it Work?
This tool adds a command interpreter plugin to the Unreal
This tool adds a command line interpreter plugin to the Unreal
Editor. You can type commands, and the plugin in the editor
will execute them. You can actually type commands directly
from the command-line. Here's an example of what you might
see:
will execute them.
```
$ ue-wingman.py Graph_Dump Graph=/Game/Testing/BP_Test,graph:EventGraph
$ ue-wingman Graph_Dump /Game/Testing/BP_Test,graph:EventGraph
node K2Node_Event_0: Event BeginPlay
output-pins OutputDelegate
@@ -32,33 +30,30 @@ see:
output-pins OutputDelegate, DeltaSeconds
```
There are tons of commands built in: Graph_Dump,
GraphNode_Add, GraphPin_Connect,
BlueprintComponent_Add, Widget_Add, and so forth.
Using these commands, it's possible to examine and modify
blueprints, widgets, and materials.
The ue-wingman command has tons of subcommands: Graph_Dump,
GraphNode_Add, GraphPin_Connect, BlueprintComponent_Add,
Widget_Add, and so forth. Using these commands, it's
possible to examine and modify blueprints, widgets, and
materials.
But, of course, these commands aren't really intended for humans.
They're intended for an AI agent. The AI is given access to these
commands using what's called "Model Context Protocol," which is
a goofy name for "a mechanism that an AI can use to send
commands to other software."
They're intended for an AI agent.
## Why Choose this Particular Unreal Engine MCP?
## Why Choose this Particular Unreal AI Plugin?
There are a *lot* of Unreal Engine MCPs out there. Some of
There are a *lot* of Unreal Engine AI plugins out there. Some of
them are, shall we say, not carefully engineered. I'm a
reasonably skilled software engineer and I've designed
this plugin to be robust and capable of sustained development.
This MCP is also designed to be as broadly general as
possible. I've seen MCPs that claim "can create 22 different
This plugin is also designed to be as broadly general as
possible. I've seen plugins that claim "can create 22 different
kinds of graph nodes!" This makes me ask: why not just
provide the *entire catalog* of all possible graph nodes?
I've seen MCPs claim "you can edit 15 different material
I've seen plugins claim "you can edit 15 different material
expression properties!" Why not provide access to *all*
editable material expression properties? I've tried to make
every tool in this MCP as capable as possible, with as few
every tool in this plugin as capable as possible, with as few
limits as possible.
Some of the MCPs out there expose the entire Unreal API to
@@ -76,15 +71,16 @@ commands.
## Installation
There are three parts to UE Wingman:
There are two parts to UE Wingman:
* The Unreal Plugin, which does 99% of the work.
* The python program "ue-wingman.py" which a human can
use to send commands to the plugin.
* The python program "ue-wingman.py"
* The python program "ue-wingman-mcp.py", which an AI
can use to send commands to the plugin.
The python program is actually less than 100 lines of code:
all it does is package up its command line arguments, send
them to the plugin, and let the plugin do the work. Then it
prints the output.
If you build Unreal from source, the best way to install the
plugin is to drop the entire UEWingman source folder into
@@ -106,25 +102,6 @@ and no other dependencies.
To install the human version, ue-wingman.py, just drop it into
a folder on your PATH.
To install the AI version, ue-wingman-mcp.py, you have to
usually set up some config file for your AI agent. I use
Claude Code, for that, you have to create a file ".mcp.json"
in your project folder, and it needs to have this inside it:
```
{
"mcpServers": {
"ue-wingman": {
"command": "python3",
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
}
}
}
```
You can usually ask your AI agent for help creating this
config file.
## The "User Manual"
You might be interested in seeing the "user manual" for the
@@ -135,18 +112,17 @@ $ ue-wingman.py Documentation_Manual
```
Of course, you're not the intended user: your AI agent is.
When the AI agent starts up ue-wingman-mcp, it is
automatically told to read the user manual. From there, the
User Manual says, among other things, that the AI agent can
get a listing of built-in commands. You can see that too:
You should put a note into your agent's system prompt to
let it know about the ue-wingman.py command, and to let
it know that it can type ue-wingman.py Documentation_Manual.
This in turn will tell your agent about this command:
```
$ ue-wingman.py Documentation_Commands
```
With these two commands at your disposal, you'll have a better
understanding of what exactly your AI agent is doing with this
plugin, and how it all works.
Using these commands, you can learn more about what this
plugin can do.
## Fun things to Try

View File

@@ -25,7 +25,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
FString Asset;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, skip reference check and force delete"))
UPROPERTY(EditAnywhere, meta=(Description="If true, skip reference check and force delete"))
bool Force = false;
virtual void Register() override

View File

@@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring to match against asset package paths"))
UPROPERTY(EditAnywhere, meta=(Description="Substring to match against asset package paths"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
UPROPERTY(EditAnywhere, meta=(Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual void Register() override

View File

@@ -32,16 +32,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
FString GraphType;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Create a new function or macro graph in a Blueprint."));
TEXT("Create a new function or macro graph in a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can be input, output, or local."));
}
virtual void Handle() override
{
@@ -79,8 +78,7 @@ public:
// Parse and validate variables before making changes
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Create the Graph
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,

View File

@@ -27,7 +27,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
FString Interface;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, keep the function graphs as regular functions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false;
virtual void Register() override

View File

@@ -281,7 +281,7 @@ public:
}
// Set the 'Class' property.
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory);
TArray<FWingProperty> Props = FWingProperty::GetVisible(Factory, true);
FWingProperty::Remove(Props, TEXT("BlueprintType"));
if (Props.Num() != 1)
{

View File

@@ -37,5 +37,6 @@ public:
if (!P) return;
WingOut::Stdout.Print(P->GetText());
WingOut::Stdout.Print(TEXT("\n"));
}
};

View File

@@ -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());
}
};

View File

@@ -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);
}
};

View File

@@ -12,19 +12,13 @@ class UWing_Documentation_Commands : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("List all the main commands with their descriptions."));
TEXT("A concise list of all ue-wingman commands."));
}
virtual void Handle() override
{
WingManual::Commands(EWingHandlerKind::Normal, Query, Verbose);
WingManual::Commands(EWingHandlerKind::Normal, TEXT(""), false);
}
};

View File

@@ -12,10 +12,10 @@ class UWing_Documentation_CreateAssets : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override

View File

@@ -12,41 +12,21 @@ class UWing_Documentation_Manual : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual"))
FString Section;
virtual void Register() override
{
TStringBuilder<128> Docs;
Docs.Append(TEXT("Print a section of the manual. Valid sections: "));
WingManual::PrintSectionNames(nullptr, WingManual::GetSections(), Docs);
UWingServer::AddHandler(this, Docs.ToString());
UWingServer::AddHandler(this, TEXT("Print the entire manual."));
}
virtual void Handle() override
{
TSet<FName> Sections = WingManual::GetSections();
if (Section.IsEmpty())
{
UWingManualSections::FetcherPaths();
UWingManualSections::ExpressingTypes();
UWingManualSections::VariableDeclarations();
UWingManualSections::EscapeSequencesInFNames();
UWingManualSections::MaterialEditing();
UWingManualSections::NodeContextMenus();
UWingManualSections::VariableGettersAndSetters();
UWingManualSections::BestPerformance();
UWingManualSections::ImportantCommands();
}
else
{
FName SectionName(*Section);
if (WingManual::PrintSection(SectionName))
{
WingOut::Stdout.Printf(TEXT("\n"));
WingManual::PrintSectionNames(TEXT("Other manual sections:"), Sections, WingOut::Stdout);
}
else
{
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"));
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
}
}
}
};

View File

@@ -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);
}
}
};

View File

@@ -28,13 +28,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
FString Dispatcher;
UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add a new event dispatcher to a Blueprint."));
TEXT("Add a new event dispatcher to a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can only be 'input'."));
}
virtual void Handle() override
{
@@ -49,7 +52,7 @@ public:
// Parse the arguments.
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Add the delegate variable
FEdGraphPinType DelegateType;

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "WingGraphActions.h"
#include "WingGraphExport.h"
@@ -16,24 +15,6 @@
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Type;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
FWingGraphAction *Action;
};
UCLASS()
class UWing_GraphNode_Add : public UWingHandler
{
@@ -43,8 +24,14 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {Type, posX, posY} objects. Use GraphNode_SearchTypes to find types."))
FWingJsonArray Nodes;
UPROPERTY(EditAnywhere, meta=(Description="Node type, from GraphNode_SearchTypes"))
FString Type;
UPROPERTY(EditAnywhere, meta=(Description="Node X position"))
int32 PosX = 0;
UPROPERTY(EditAnywhere, meta=(Description="Node Y position"))
int32 PosY = 0;
virtual void Register() override
{
@@ -58,38 +45,19 @@ public:
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num();
FWingGraphActions GraphActions(TargetGraph);
TArray<FWingGraphAction*> Results = GraphActions.Search(Type, 2, true);
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Type, WingOut::Stdout)) return;
// Parse the json array, turning it into an array of spawn node entries.
TArray<FSpawnNodeEntry> Entries;
FSpawnNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;
TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.Type, 2, true);
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Entry.Type, WingOut::Stdout)) return;
Entry.Action = Results[0];
Entries.Add(Entry);
}
// Execute all.
for (const FSpawnNodeEntry &Entry : Entries)
{
UEdGraphNode* NewNode = Entry.Action->Execute(FVector2D(Entry.PosX, Entry.PosY));
UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(PosX, PosY));
if (NewNode)
{
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Entry.Type);
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *Type);
WingGraphExport Export(NewNode, false, true);
WingOut::Stdout.Print(Export.GetOutput());
return;
}
else
{
WingOut::Stdout.Printf(TEXT("Failed: %s\n\n"), *Entry.Type);
continue;
}
}
WingOut::Stdout.Printf(TEXT("Failed: %s\n"), *Type);
}
};

View File

@@ -21,7 +21,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View File

@@ -19,15 +19,15 @@ class UWing_GraphNode_SearchTypes : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain * wildcards"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Query strings; each may contain * wildcards"))
FWingRestOfArgv Queries;
virtual void Register() override
{
UWingServer::AddHandler(this,
@@ -41,6 +41,9 @@ public:
if (!TargetGraph) return;
FWingGraphActions GraphActions(TargetGraph);
for (const FString& Query : Queries.Argv)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
for (const FWingGraphAction* Action : Results)
{
@@ -56,4 +59,5 @@ public:
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
}
}
}
};

View File

@@ -4,36 +4,15 @@
#include "WingBasics.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "GraphNode_SetDefaults.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetNodeDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString Name;
UPROPERTY()
FString Value;
};
#include "GraphNode_SetDefault.generated.h"
UCLASS()
class UWing_GraphNode_SetDefaults : public UWingHandler
class UWing_GraphNode_SetDefault : public UWingHandler
{
GENERATED_BODY()
@@ -41,8 +20,14 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {node, name, value} objects"))
FWingJsonArray Pins;
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Description="Pin or property name"))
FString Name;
UPROPERTY(EditAnywhere, meta=(Description="New default value"))
FString Value;
virtual void Register() override
{
@@ -53,15 +38,15 @@ public:
// K2 graphs: set pin default values.
// -----------------------------------------------------------------------
void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema)
void HandleK2(UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema)
{
WingFetcher F(GraphObj, WingOut::Stdout);
UWingGraphPinRef* PinRef = F.Node(Entry.Node).Pin(Entry.Name).Cast<UWingGraphPinRef>();
UWingGraphPinRef* PinRef = F.Node(Node).Pin(Name).Cast<UWingGraphPinRef>();
if (!PinRef) return;
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
if (!Pin) return;
UEdGraphNode* Node = Pin->GetOwningNode();
UEdGraphNode* FoundNode = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
@@ -72,34 +57,34 @@ public:
FString UseDefaultValue;
TObjectPtr<UObject> UseDefaultObject = nullptr;
FText UseDefaultText;
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, FoundNode, Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
if (!Error.IsEmpty())
{
WingOut::Stdout.Printf(TEXT("error: %s: %s\n"), *WingUtils::FormatName(Pin), *Error);
return;
}
UWingServer::AddTouchedObject(Node);
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
UWingServer::AddTouchedObject(FoundNode);
K2Schema->TrySetDefaultValue(*Pin, Value);
}
// -----------------------------------------------------------------------
// Material graphs: set material expression properties.
// -----------------------------------------------------------------------
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj)
void HandleMaterial(UEdGraph* GraphObj)
{
WingFetcher F(GraphObj, WingOut::Stdout);
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) return;
UEdGraphNode* FoundNode = F.Node(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
TArray<FWingProperty> All = FWingProperty::GetDetails(Node, true);
FWingProperty *P = WingUtils::FindOneWithExternalID(Entry.Name, All, TEXT("Property"), WingOut::Stdout);
TArray<FWingProperty> All = FWingProperty::GetDetails(FoundNode, true);
FWingProperty *P = WingUtils::FindOneWithExternalID(Name, All, TEXT("Property"), WingOut::Stdout);
if (!P) return;
UWingServer::AddTouchedObject(Node);
UWingServer::AddTouchedObject(FoundNode);
if (!P->SetText(Entry.Value, WingOut::Stdout))
if (!P->SetText(Value, WingOut::Stdout))
return;
}
@@ -122,14 +107,8 @@ public:
return;
}
FSetNodeDefaultEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *PinVal, false, WingOut::Stdout)) continue;
if (K2Schema) HandleK2Entry(Entry, GraphObj, K2Schema);
else if (MGSchema) HandleMaterialEntry(Entry, GraphObj);
}
if (K2Schema) HandleK2(GraphObj, K2Schema);
else if (MGSchema) HandleMaterial(GraphObj);
WingOut::Stdout.Printf(TEXT("Done.\n"));
}

View File

@@ -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"));
}
};

View File

@@ -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());
}
};

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
@@ -12,23 +11,6 @@
#include "GraphPin_Connect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS()
class UWing_GraphPin_Connect : public UWingHandler
{
@@ -38,8 +20,8 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {sourcePin, targetPin} objects"))
FWingJsonArray Connections;
UPROPERTY(EditAnywhere, meta=(Description="Alternating source pin / target pin strings"))
FWingRestOfArgv SourcePin_TargetPin;
virtual void Register() override
{
@@ -54,24 +36,27 @@ public:
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalCount = Connections.Array.Num();
FConnectPinsEntry Entry;
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(&Entry);
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
if ((SourcePin_TargetPin.Argv.Num() % 2) != 0)
{
if (!FWingProperty::PopulateFromJson(EntryProps, *ConnVal, false, WingOut::Stdout))
continue;
WingOut::Stdout.Print(TEXT("ERROR: SourcePin_TargetPin must contain an even number of arguments.\n"));
return;
}
int32 SuccessCount = 0;
int32 TotalCount = SourcePin_TargetPin.Argv.Num() / 2;
for (int32 I = 0; I < SourcePin_TargetPin.Argv.Num(); I += 2)
{
const FString& SourcePinPath = SourcePin_TargetPin.Argv[I];
const FString& TargetPinPath = SourcePin_TargetPin.Argv[I + 1];
WingFetcher FS(G, WingOut::Stdout);
UWingGraphPinRef* SourcePinRef = FS.Walk(Entry.SourcePin).Cast<UWingGraphPinRef>();
UWingGraphPinRef* SourcePinRef = FS.Walk(SourcePinPath).Cast<UWingGraphPinRef>();
if (!SourcePinRef) continue;
UEdGraphPin* SourcePin = WingUtils::CheckGetPin(SourcePinRef->Node, SourcePinRef->PinName, WingOut::Stdout);
if (!SourcePin) continue;
WingFetcher FT(G, WingOut::Stdout);
UWingGraphPinRef* TargetPinRef = FT.Walk(Entry.TargetPin).Cast<UWingGraphPinRef>();
UWingGraphPinRef* TargetPinRef = FT.Walk(TargetPinPath).Cast<UWingGraphPinRef>();
if (!TargetPinRef) continue;
UEdGraphPin* TargetPin = WingUtils::CheckGetPin(TargetPinRef->Node, TargetPinRef->PinName, WingOut::Stdout);
if (!TargetPin) continue;

View File

@@ -4,7 +4,6 @@
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
@@ -24,8 +23,8 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of pin ID strings"))
FWingJsonArray Pins;
UPROPERTY(EditAnywhere, meta=(Description="Pin ID strings"))
FWingRestOfArgv Pins;
virtual void Register() override
{
@@ -43,15 +42,8 @@ public:
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
for (const FString& PinPath : Pins.Argv)
{
FString PinPath;
if (!PinVal->TryGetString(PinPath))
{
WingOut::Stdout.Print(TEXT("ERROR: Expected a string pin ID.\n"));
continue;
}
WingFetcher FP(G, WingOut::Stdout);
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
if (!PinRef) continue;
@@ -72,6 +64,6 @@ public:
}
WingOut::Stdout.Printf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
SuccessCount, Pins.Array.Num(), TotalDisconnected);
SuccessCount, Pins.Argv.Num(), TotalDisconnected);
}
};

View File

@@ -22,7 +22,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View 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());
}
};

View File

@@ -20,7 +20,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results"))
int32 Limit = 100;
virtual void Register() override

View File

@@ -21,22 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add new variables. Format: 'type name (flags) = default', one per line."));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Create(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,23 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Modify variables of a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -47,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Modify(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,22 +21,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variable names to remove, comma-separated"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variable names to remove, comma-separated"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variable names to remove, comma-separated"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Remove variables from a blueprint, graph, or custom event node."));
TEXT("Remove variables from a blueprint, graph, or custom event node. "
"Each variable must be expressed as: 'kind name'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +39,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseNamesString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseNamesString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseNamesString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, true, WingOut::Stdout)) return;
if (!Vars.Remove(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));
}

View File

@@ -33,10 +33,10 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
FString Name;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Parent widget name. If omitted, sets as root."))
UPROPERTY(EditAnywhere, meta=(Description="Parent widget name. If omitted, sets as root."))
FString Parent;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Whether to expose the widget as a variable in the blueprint (default false)"))
UPROPERTY(EditAnywhere, meta=(Description="Whether to expose the widget as a variable in the blueprint (default false)"))
bool IsVariable = false;
virtual void Register() override
@@ -117,6 +117,10 @@ public:
BP->WidgetTree->RootWidget = NewWidget;
}
// Register a variable GUID for the new widget. UMG's compiler
// ensures every widget in the tree is present in this map.
// BP->OnVariableAdded(NewWidget->GetFName());
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
}
};

View File

@@ -17,12 +17,12 @@ class UWing_Widget_SearchTypes : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Query string, can contain *"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results per query"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Query strings; each may contain *"))
FWingRestOfArgv Queries;
virtual void Register() override
{
UWingServer::AddHandler(this,
@@ -32,6 +32,9 @@ public:
virtual void Handle() override
{
WingWidgets Widgets;
for (const FString& Query : Queries.Argv)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
for (const WingWidgets::Type& Entry : Results)
{
@@ -47,4 +50,5 @@ public:
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
}
}
}
};

View File

@@ -1,6 +1,7 @@
#include "WingFetcher.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "WingComponent.h"
#include "Engine/Blueprint.h"
@@ -368,36 +369,27 @@ WingFetcher& WingFetcher::StructProp(const FString& Value)
return SetError();
}
FStructProperty* StructProp = nullptr;
TArray<FWingProperty> Details =
FWingProperty::GetDetails(Obj.Get(), true);
// The "host" is the object containing this property.
UObject *HostObject = Obj.Get();
void *HostBase = Obj.Get();
UStruct *HostType = Obj.Get()->GetClass();
bool HostEditable = true;
FWingProperty *WProp = WingUtils::FindOneWithInternalID(
InternalID, Details, TEXT("Property"), WingOut::Stdout);
if (!WProp) return SetError();
// If we are *already* inside a UWingStructRef, update the host
// fields, to make it possible to navigate even further inside.
if (UWingStructRef *SPtr = ::Cast<UWingStructRef>(Obj.Get()))
if (FStructProperty *FSProp = CastField<FStructProperty>(WProp->Prop))
{
HostObject = SPtr->Object;
HostBase = SPtr->StructBase;
HostType = SPtr->StructType;
HostEditable = SPtr->Editable;
}
StructProp = FindFProperty<FStructProperty>(HostType, InternalID);
if (!StructProp)
{
Errors.Printf(TEXT("ERROR: No struct property '%s' found on %s\n"), *Value, *HostType->GetName());
return SetError();
}
UWingStructRef* Ptr = NewObject<UWingStructRef>();
Ptr->Object = HostObject;
Ptr->StructType = StructProp->Struct;
Ptr->StructBase = StructProp->ContainerPtrToValuePtr<void>(HostBase);
Ptr->Editable = HostEditable && StructProp->HasAllPropertyFlags(CPF_Edit);
Ptr->Object = WProp->Object.Get();
Ptr->StructType = FSProp->Struct;
Ptr->StructBase = FSProp->ContainerPtrToValuePtr<void>(WProp->Container);
Ptr->Editable = WProp->Editable;
SetObj(Ptr);
return *this;
}
else
{
Errors.Printf(TEXT("Property %s is not a struct property.\n"),
*WProp->Prop->GetName());
return SetError();
}
}

View File

@@ -6,17 +6,23 @@
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
{
WingOut::Stdout.Print(TEXT("ue-wingman "));
WingOut::Stdout.Print(Handler.Name);
WingOut::Stdout.Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) WingOut::Stdout.Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?"));
WingOut::Stdout.Print(PropIt->GetName());
FStructProperty* StructProp = CastField<FStructProperty>(*PropIt);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
if (bIsRest)
{
WingOut::Stdout.Printf(TEXT(" [%s...]"), *PropIt->GetName());
}
WingOut::Stdout.Print(TEXT(")\n"));
else
{
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
}
}
WingOut::Stdout.Print(TEXT("\n"));
}
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
@@ -26,27 +32,17 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
{
FProperty* Prop = *PropIt;
FString Name = Prop->GetName();
FString Type = UWingTypes::TypeToText(Prop);
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
FString Desc = Prop->GetMetaData(TEXT("Description"));
if (Desc.IsEmpty()) Desc = TEXT("No documentation");
if (bOptional)
{
WingOut::Stdout.Printf(TEXT(" %s (optional %s)"), *Name, *Type);
}
else
{
WingOut::Stdout.Printf(TEXT(" %s (%s)"), *Name, *Type);
}
if (!Desc.IsEmpty()) WingOut::Stdout.Printf(TEXT(" — %s"), *Desc);
WingOut::Stdout.Print(TEXT("\n"));
WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc);
}
}
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
{
if (Handler.Documentation.IsEmpty()) return;
WingOut::Stdout.Print(WingUtils::WrapText(Handler.Documentation, 80, TEXT(" // ")));
WingOut::Stdout.Printf(TEXT("\n%s\n\n"), *Handler.Documentation);
}
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
@@ -118,13 +114,22 @@ void UWingManualSections::VariableDeclarations()
WingOut::Stdout.Print(TEXT(
"\n VARIABLE DECLARATIONS:"
"\n"
"\n We have our own syntax for variable declarations: a type,"
"\n a name, optional flags, and an optional default value,"
"\n always on one line:"
"\n We have our own syntax for variable declarations:"
"\n"
"\n Array<Actor> Actors"
"\n Float F (InstanceEditable)"
"\n String S = This is the default value"
"\n kind type name (optional flags) = optional default value"
"\n"
"\n Kind can be:"
"\n"
"\n blueprint - eg, instance variables"
"\n input - a function argument or macro input"
"\n output - a function return value or macro output"
"\n local - local variables"
"\n"
"\n Here are some examples:"
"\n"
"\n input Array<Actor> Actors"
"\n output Float F (InstanceEditable)"
"\n blueprint String S = This is the default value"
"\n"
"\n The commands Variables_Add, Variables_Modify,"
"\n and Variables_Remove can be used to edit "
@@ -199,22 +204,33 @@ void UWingManualSections::VariableGettersAndSetters()
));
}
void UWingManualSections::BestPerformance()
{
WingOut::Stdout.Print(TEXT(
"\n BEST PERFORMANCE:"
"\n"
"\n UE Wingman is much faster than the LLM. Therefore, it is"
"\n advantageous to batch: chain multiple ue-wingman commands"
"\n together using bash semicolon."
"\n"
));
}
void UWingManualSections::ImportantCommands()
{
WingOut::Stdout.Print(TEXT(
"\n IMPORTANT COMMANDS:"
"\n"
"\n Documentation_Manual: print manual sections"
"\n Documentation_Commands: a list of all the main commands"
"\n Documentation_CreateAssets: Additional commands that create new assets"
"\n Documentation_Manual: print the entire manual"
"\n Documentation_Section: print a single section of the manual"
"\n Documentation_Commands: print concise list of all ue-wingman commands"
"\n Documentation_Command: detailed documentation for a single ue-wingman command"
"\n Documentation_CreateAssets: list of commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n"
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
"\n to get detailed help for a specific command."
"\n"
));
}
@@ -255,10 +271,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
FString QueryLower = Query.ToLower();
FString PrevGroup;
bool any = false;
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
{
if (H.Kind != Kind) continue;
if (!H.Name.ToLower().Contains(QueryLower)) continue;
any = true;
// Blank line between groups
if (!Verbose)
@@ -277,4 +295,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
else
PrintHandlerPrototype(H);
}
if (!any)
{
WingOut::Stdout.Print(TEXT("No matching commands. To see a full list, type:\n"));
WingOut::Stdout.Print(TEXT(" ue-wingman Documentation_Commands.\n"));
}
}

View File

@@ -265,7 +265,7 @@ bool FWingParameterEditor::AddOverride(
// Parse the value string.
FWingParameterEditor Editor;
if (!FWingProperty(GS->Property, &Editor, true).SetText(StrVal, Errors)) return false;
if (!FWingProperty(nullptr, &Editor, GS->Property, true).SetText(StrVal, Errors)) return false;
Meta->Value = GS->Getter(Editor);
// Apply the update.
@@ -308,7 +308,7 @@ void FWingParameterEditor::Print(const Info &ID, const Metadata &Meta)
}
FWingParameterEditor Editor;
GS->Setter(Editor, Meta.Value);
FString StrVal = FWingProperty(GS->Property, &Editor, false).GetText();
FString StrVal = FWingProperty(nullptr, &Editor, GS->Property, false).GetText();
WingOut::Stdout.Printf(TEXT(" %s %s\n"),
*StringID(ID), *StrVal);
}

View File

@@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
return true;
}
bool FWingProperty::SetJson(const FJsonValue &JsonValue, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
if (JsonValue.Type == EJson::String)
{
return SetText(JsonValue.AsString(), Errors);
}
if (JsonValue.Type == EJson::Number)
{
return SetDouble(JsonValue.AsNumber(), Errors);
}
if (JsonValue.Type == EJson::Boolean)
{
return SetBool(JsonValue.AsBool(), Errors);
}
if (JsonValue.Type == EJson::Object)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonObject::StaticStruct()))
{
FWingJsonObject Val;
Val.Json = JsonValue.AsObject();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json object"), Errors);
return false;
}
if (JsonValue.Type == EJson::Array)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonArray::StaticStruct()))
{
FWingJsonArray Val;
Val.Array = JsonValue.AsArray();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json array"), Errors);
return false;
}
PrintExpectsReceived(TEXT("Unrecognized Json Data"), Errors);
return false;
}
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
{
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
@@ -341,7 +290,11 @@ FString FWingProperty::GetText() const
return TEXT("None");
}
FString Result;
Prop->ExportTextItem_InContainer(Result, Container, nullptr, nullptr, PPF_None);
// DefaultValue == PropertyValue makes ExportText_Direct's Data==Delta
// short-circuit fire for every subfield, so default-valued fields still
// get emitted (e.g. SizeRule=Automatic on FSlateChildSize).
const void* Value = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, nullptr, PPF_None);
return Result;
}
@@ -374,25 +327,25 @@ FString FWingProperty::GetCategory() const
return Result;
}
TArray<FWingProperty> FWingProperty::GetAll(FWingStructAndUStruct Obj)
TArray<FWingProperty> FWingProperty::GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
{
TArray<FWingProperty> Result;
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
for (TFieldIterator<FProperty> It(Struct); It; ++It)
{
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Emplace(Obj, Container, *It, Editable);
}
return Result;
}
TArray<FWingProperty> FWingProperty::GetVisible(FWingStructAndUStruct Obj)
TArray<FWingProperty> FWingProperty::GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
{
TArray<FWingProperty> Result;
for (TFieldIterator<FProperty> It(Obj.UStructPtr); It; ++It)
for (TFieldIterator<FProperty> It(Struct); It; ++It)
{
if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
bool Editable = !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Add(FWingProperty(*It, Obj.StructPtr, Editable));
bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
Result.Emplace(Obj, Container, *It, Editable);
}
return Result;
}
@@ -432,10 +385,8 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
// of the struct instead. Propagate editability of the host.
if (UWingStructRef *SP = Cast<UWingStructRef>(Obj))
{
TArray<FWingProperty> Result =
GetVisible(FWingStructAndUStruct(SP->StructBase, SP->StructType));
if (!Mutable || (!SP->Editable)) StripEditable(Result);
return Result;
return GetVisible(SP->Object, SP->StructBase,
SP->StructType, Mutable && SP->Editable);
}
// Blueprints don't have editable properties. So
@@ -461,7 +412,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
}
}
TArray<FWingProperty> Result = GetVisible(Obj);
TArray<FWingProperty> Result = GetVisible(Obj, Mutable);
// If it's a Material Graph node, also collect properties from
// the associated material expression.
@@ -470,7 +421,7 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
Result.Append(GetVisible(Expr));
Result.Append(GetVisible(Expr, Mutable));
}
}
@@ -482,66 +433,63 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
FWingProperty::Remove(Result, TEXT("Slot"));
if (UPanelSlot* Slot = Widget->Slot)
{
Result.Append(GetVisible(Slot));
Result.Append(GetVisible(Slot, Mutable));
}
FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable"));
if (VarProp) Result.Add(FWingProperty(VarProp, Widget, true));
if (VarProp) Result.Emplace(Widget, Widget, VarProp, true);
}
if (!Mutable) StripEditable(Result);
return Result;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors)
bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
{
bool Ok = true;
int32 ArgIndex = 0;
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
{
FWingProperty& P = Props[PropIndex];
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
// Build a set of known property names for the unknown-field check.
TSet<FName> KnownKeys;
for (const FWingProperty& P : Props) KnownKeys.Add(P->GetFName());
// Check for unknown fields in the JSON
for (const auto& KV : Json.Values)
if (bIsRest)
{
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
if (!KnownKeys.Contains(Name))
if (PropIndex + 1 != Props.Num())
{
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
Ok = false;
}
}
// Populate each property from JSON
for (FWingProperty& P : Props)
{
FString JsonKey = WingUtils::FormatName(P.Prop);
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
if (!Value)
{
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
if (!Optional)
{
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
Ok = false;
}
continue;
}
if (!P.SetJson(*Value, Errors)) Ok = false;
}
return Ok;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
{
// Make sure they passed in a JSON object.
TSharedPtr<FJsonObject> Obj = Json.AsObject();
if (Obj == nullptr)
{
Errors.Printf(TEXT("property data should be stored in a json object\n"));
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
FWingRestOfArgv Rest;
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
{
Rest.Argv.Add(Argv[I]);
}
P.Prop->SetValue_InContainer(P.Container, &Rest);
ArgIndex = Argv.Num();
continue;
}
if (ArgIndex >= Argv.Num())
{
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
ArgIndex++;
}
if (ArgIndex < Argv.Num())
{
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
*Argv[ArgIndex]);
return false;
}
return true;
}
@@ -576,11 +524,6 @@ bool FWingProperty::CheckImportTextResult(const FString &Value, WingOut Errors)
return true;
}
void FWingProperty::StripEditable(TArray<FWingProperty> &Props)
{
for (FWingProperty &Elt : Props) Elt.Editable = false;
}
bool FWingProperty::CheckEditable(WingOut Errors) const
{
if (!Editable)

View File

@@ -4,6 +4,7 @@
#include "UObject/StrongObjectPtr.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Misc/CoreDelegates.h"
#include "Misc/OutputDeviceRedirector.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
@@ -55,8 +56,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
return;
}
BuildWingHandlerRegistry();
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddUObject(this, &UWingServer::OnModulesChanged);
LoadingPhasesCompleteHandle = FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UWingServer::BuildWingHandlerRegistry);
LogCapture.bEnabled = false;
GLog->AddOutputDevice(&LogCapture);
bRunning = true;
@@ -65,7 +65,7 @@ void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
void UWingServer::Deinitialize()
{
FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle);
FCoreDelegates::OnAllModuleLoadingPhasesComplete.Remove(LoadingPhasesCompleteHandle);
if (!bRunning)
{
@@ -81,7 +81,7 @@ void UWingServer::Deinitialize()
bShuttingDown = true;
for (auto& Msg : PendingMessages)
{
Msg->Response.SetValue(FString());
Msg->Response.SetValue(TArray<uint8>());
}
PendingMessages.Empty();
}
@@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime)
// If we have a request, process it.
if (Request.IsValid())
{
FString Response = HandleRequest(Request->Line);
TArray<uint8> Response = HandleRequest(Request->Request);
Request->Response.SetValue(Response);
}
}
@@ -169,7 +169,24 @@ TStatId UWingServer::GetStatId() const
// HandleRequest — Given a command, execute it.
// ============================================================
FString UWingServer::HandleRequest(const FString& Line)
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
{
TArray<FString> Argv;
FString ResponseText;
if (DeserializeArgv(RequestBytes, Argv))
{
PreCallHandler();
TryCallHandler(Argv);
ResponseText = PostCallHandler();
}
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
FTCHARToUTF8 Utf8(*ResponseText);
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
}
void UWingServer::PreCallHandler()
{
LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true;
@@ -177,9 +194,10 @@ FString UWingServer::HandleRequest(const FString& Line)
SuggestedManualSections.Empty();
bSuggestHandlerHelp = false;
LastHandler = nullptr;
}
TryCallHandler(Line);
FString UWingServer::PostCallHandler()
{
Notifier.SendNotifications();
LogCapture.bEnabled = false;
for (const FString& Msg : LogCapture.CapturedErrors)
@@ -202,34 +220,24 @@ FString UWingServer::HandleRequest(const FString& Line)
}
FString Result = WingOut::StdoutBuffer.ToString();
WingOut::StdoutBuffer.Reset();
for (int32 i = 0; i < Result.Len(); ++i)
{
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
}
return Result;
}
void UWingServer::TryCallHandler(const FString &Line)
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
{
// Turn the request string into a JSON tree.
TSharedPtr<FJsonObject> Request;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
FJsonSerializer::Deserialize(Reader, Request);
if (!Request.IsValid())
FString Command = "Documentation_Manual";
if (Argv.Num() > 0)
{
WingOut::Stdout.Printf(TEXT("Request is not valid JSON"));
return;
Command = Argv[0];
Argv = Argv.RightChop(1);
}
// Extract the command from the request.
FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command))
if ((Command.Equals(TEXT("--help"))) ||
(Command.Equals(TEXT("-help"))) ||
(Command.Equals(TEXT("help"))))
{
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
return;
Command = "Documentation_Manual";
}
Request->RemoveField(TEXT("command"));
// Find the handler for the specified command.
FWingHandlerConfig* Found = FindHandler(Command);
@@ -246,9 +254,9 @@ void UWingServer::TryCallHandler(const FString &Line)
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
Handler->Configuration = Found;
// Populate the handler object with the request parameters.
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler);
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
// Populate the handler object with argv parameters.
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
if (!FWingProperty::PopulateFromArgv(Props, Argv, WingOut::Stdout))
{
UWingServer::SuggestHandlerHelp();
return;
@@ -306,102 +314,105 @@ void UWingServer::CleanupFinishedClients()
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
FSocket* Socket = Client->Socket;
TArray<uint8> RecvBuf;
RecvBuf.SetNumUninitialized(MinUnusedRecvSpace);
int32 RecvLen = 0;
WaitForAssetRegistry();
while (true)
TArray<uint8> Request;
if (!ReceiveRequest(Socket, Request))
{
FString Request;
if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request))
{
FString Response;
Client->bDone = true;
return;
}
TArray<uint8> Response;
if (!ProcessRequestOnGameThread(Request, Response))
{
Client->bDone = true;
return;
}
// Write the response back, null-terminated (blocking)
FTCHARToUTF8 Utf8(*Response);
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
Utf8.Length() + 1))
{
Client->bDone = true;
return;
}
continue;
}
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
{
break;
}
}
SendAll(Socket, Response.GetData(), Response.Num());
Client->bDone = true;
}
bool UWingServer::ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest)
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
{
const uint8* EndOfRequest = static_cast<const uint8*>(
memchr(RecvBuf.GetData(), '\0', RecvLen));
if (EndOfRequest == nullptr)
return
((uint32)Data[0] << 24) |
((uint32)Data[1] << 16) |
((uint32)Data[2] << 8) |
(uint32)Data[3];
}
bool UWingServer::DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
{
Argv.Empty();
int32 Offset = 0;
while (Offset < RequestBytes.Num())
{
if (RequestBytes.Num() - Offset < 4)
{
Argv.Empty();
return false;
}
const int32 MessageLen =
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
OutRequest = FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
if (RemainingBytes > 0)
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
Offset += 4;
if ((uint32)(RequestBytes.Num() - Offset) < Length)
{
FMemory::Memmove(
RecvBuf.GetData(),
RecvBuf.GetData() + MessageLen + 1,
RemainingBytes);
Argv.Empty();
return false;
}
RecvLen = RemainingBytes;
Argv.Add(FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
Length));
Offset += (int32)Length;
}
return true;
}
bool UWingServer::ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen)
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
constexpr int32 ChunkSize = 8192;
int32 UnusedSpace = RecvBuf.Num() - RecvLen;
if (UnusedSpace < MinUnusedRecvSpace)
{
if (RecvBuf.Num() >= MaxRecvBufBytes)
{
return false;
}
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
UnusedSpace = RecvBuf.Num() - RecvLen;
}
TArray<uint8> RecvBuf;
RecvBuf.Reserve(ChunkSize);
// Unreal's FSocket API is fundamentally broken: recv cannot
// differentiate between a socket that has been cleanly closed
// and a socket that has had an error. So we have no choice
// but to just read until recv returns false (which could be a
// clean close or an error). Then, we check if we have a cleanly
// encoded payload: if so, we assume everything is fine.
while (true)
{
uint8 Temp[ChunkSize];
int32 BytesRead = 0;
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
{
break;
}
if (BytesRead <= 0) break;
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
{
return false;
}
if (BytesRead <= 0)
{
return false;
RecvBuf.Append(Temp, BytesRead);
}
RecvLen += BytesRead;
if (RecvBuf.Num() < 4) return false;
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
RecvBuf.RemoveAt(0, 4);
OutRequest = MoveTemp(RecvBuf);
return true;
}
@@ -421,13 +432,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
}
bool UWingServer::ProcessRequestOnGameThread(
const FString& Request, FString& Response)
const TArray<uint8>& Request, TArray<uint8>& Response)
{
// Enqueue the message for game-thread processing.
TSharedPtr<UWingServer::FPendingMessage> Msg =
MakeShared<UWingServer::FPendingMessage>();
Msg->Line = Request;
TFuture<FString> Future = Msg->Response.GetFuture();
Msg->Request = Request;
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
{
FScopeLock Lock(&GWingServer->Mutex);
@@ -484,11 +495,6 @@ void UWingServer::BuildWingHandlerRegistry()
WingHandlerRegistry.Sort([](const FWingHandlerConfig& A, const FWingHandlerConfig& B) { return A.Name < B.Name; });
}
void UWingServer::OnModulesChanged(FName ModuleName, EModuleChangeReason Reason)
{
BuildWingHandlerRegistry();
}
FWingHandlerConfig* UWingServer::FindHandler(const FString& Name)
{
int32 Index = Algo::LowerBoundBy(WingHandlerRegistry, Name, [](const FWingHandlerConfig& H) { return H.Name; });

View File

@@ -82,7 +82,7 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
{
if ((!Allow) && (!Variables.IsEmpty()))
{
Errors.Printf(TEXT("In this context, %s must be empty."), ListName);
Errors.Printf(TEXT("This object does not support %s.\n"), ListName);
return false;
}
for (const Var &Variable : Variables)
@@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
return true;
}
bool WingVariableList::ParseString(const FString &Input, WingOut Errors)
{
Variables.Empty();
TArray<FString> Lines;
Input.ParseIntoArrayLines(Lines);
for (const FString& Line : Lines)
{
WingTokenizer Tok(Line);
if (Tok.NextType() == 0) continue;
Var V;
V.DefaultSpecified = false;
if (!ParseOneVariable(Tok, V, Errors)) return false;
Variables.Add(MoveTemp(V));
}
return true;
}
bool WingVariableList::ParseNamesString(const FString &Input, WingOut Errors)
{
Variables.Empty();
WingTokenizer Tok(Input);
while (Tok.TokenIs(Tok.Identifier))
{
FName Name = Tok.NextName();
Var V;
V.Name = Name;
Variables.Add(V);
V.DefaultSpecified = false;
Tok.Advance();
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(0))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("Unexpected token %s in variable list"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors)
{
// Parse type.
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if (Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (Tok.NextType() == Tok.RestOfLine)
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
void WingVariables::Empty()
{
BlueprintVariables.Empty();
@@ -254,6 +144,107 @@ void WingVariables::Print(WingOut Out)
OutputVariables.Print(Out);
}
WingVariableList *WingVariables::GetList(FName Name)
{
if (Name == TEXT("blueprint")) return &BlueprintVariables;
if (Name == TEXT("input")) return &InputVariables;
if (Name == TEXT("output")) return &OutputVariables;
if (Name == TEXT("local")) return &LocalVariables;
return nullptr;
}
bool WingVariables::ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors)
{
// Parse Kind.
if (GetList(Tok.NextName()) == nullptr)
{
Errors.Print(TEXT("ERROR: Variable description should start with 'blueprint', 'input', 'output', or 'local'"));
return false;
}
Kind = Tok.NextName();
Tok.Advance();
// Parse type.
if (!NameOnly)
{
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
}
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if ((!NameOnly) && Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (!NameOnly && (Tok.NextType() == Tok.RestOfLine))
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariables::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
bool WingVariables::Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors)
{
for (const FString& Onevar : Vars)
{
WingTokenizer Tok(Onevar);
FName Kind;
Var V;
if (!ParseOneVariable(Tok, Kind, V, NameOnly, Errors)) return false;
WingVariableList *List = GetList(Kind);
List->Add(V);
}
return true;
}
void WingVariables::Load(WingOut Errors)
{
Empty();
@@ -325,7 +316,7 @@ WingVariables::Var WingVariables::LoadBlueprintVariableDescription(FBPVariableDe
FProperty* Prop = CDO->GetClass()->FindPropertyByName(Desc.VarName);
if (Prop)
{
Result.DefaultValue = FWingProperty(Prop, CDO, false).GetText();
Result.DefaultValue = FWingProperty(CDO, CDO, Prop, false).GetText();
Result.DefaultSpecified = true;
}
}
@@ -500,7 +491,7 @@ bool WingVariables::ModifyBlueprintDefaults(WingOut Errors)
*WingTokenizer::ExternalizeID(Input.Name));
return false;
}
if (!FWingProperty(Prop, CDO, true).SetText(Input.DefaultValue, Errors)) return false;
if (!FWingProperty(CDO, CDO, Prop, true).SetText(Input.DefaultValue, Errors)) return false;
}
}
return true;

View File

@@ -62,32 +62,18 @@ public:
////////////////////////////////////////////////////////////
//
// Json wrappers.
//
// Normally, the json request is automatically used to
// populate the properties of the handler, so the handler
// doesn't have to deal with json. However, in a few cases,
// the handler actually does want to see some json. These
// wrappers allow a handler to request raw json data instead
// of pre-processed values.
// A simple type to store the remaining arguments in
// an Argv Array.
//
////////////////////////////////////////////////////////////
USTRUCT()
struct FWingJsonObject
struct FWingRestOfArgv
{
GENERATED_BODY()
TSharedPtr<FJsonObject> Json;
};
// Marker struct for handler parameters that accept a JSON array.
// PopulateFromJson stashes the actual JSON array into the Array field.
//
USTRUCT()
struct FWingJsonArray
{
GENERATED_BODY()
TArray<TSharedPtr<FJsonValue>> Array;
UPROPERTY()
TArray<FString> Argv;
};
////////////////////////////////////////////////////////////
@@ -129,38 +115,6 @@ private:
FStringBuilderBase *Buffer;
};
////////////////////////////////////////////////////////////
//
// FWingStructAndUStruct.
//
// A pointer to a struct, and also a pointer to a UStruct
// that describes the struct. This also can store a
// UObject and the UClass that describes the UObject.
//
////////////////////////////////////////////////////////////
// A FWingStructAndUStruct is a pointer to a struct and its associated ustruct.
//
struct FWingStructAndUStruct
{
void *StructPtr;
UStruct *UStructPtr;
// Explicit constructor.
explicit FWingStructAndUStruct(void *Base, UStruct *S) : StructPtr(Base), UStructPtr(S) {}
// Copy constructor.
FWingStructAndUStruct(FWingStructAndUStruct &Src) : StructPtr(Src.StructPtr), UStructPtr(Src.UStructPtr) {}
// Construct from a UObject.
FWingStructAndUStruct(UObject *Obj) : StructPtr(Obj), UStructPtr(Obj->GetClass()) {}
// Construct from a UStruct pointer.
template<class T, typename = std::enable_if_t<!std::is_base_of_v<UObject, T>>>
FWingStructAndUStruct(T *Struct) : StructPtr(Struct), UStructPtr(Struct->StaticStruct()) {}
};
////////////////////////////////////////////////////////////
//
// References.
@@ -225,10 +179,11 @@ public:
UPROPERTY()
UObject* Object;
void *StructBase;
UPROPERTY()
UStruct* StructType;
void *StructBase;
bool Editable;
};

View File

@@ -48,4 +48,7 @@ public:
UFUNCTION()
static void VariableGettersAndSetters();
UFUNCTION()
static void BestPerformance();
};

View File

@@ -3,20 +3,24 @@
#include "CoreMinimal.h"
#include "WingBasics.h"
// A resolved property: the FProperty descriptor plus a pointer to
// the value's storage.
//
struct FWingProperty
{
// To understand the following fields, imagine an object
// that contains a struct, which contains another struct,
// which contains another struct, which contains a field F.
// In that case, Object points to the object that
// contains everything, whereas Container points to the
// innermost struct that contains the property.
FProperty* Prop = nullptr;
TStrongObjectPtr<UObject> Object = nullptr;
void* Container = nullptr;
FProperty* Prop = nullptr;
bool Editable = false;
// Construct a property reference.
//
FWingProperty(FProperty* InProp, void* InContainer, bool Edit)
: Prop(InProp), Container(InContainer), Editable(Edit) {}
FWingProperty(UObject *InObject, void* InContainer, FProperty* InProp, bool Edit)
: Object(InObject), Container(InContainer), Prop(InProp), Editable(Edit) {}
// Construct a null property reference.
//
@@ -37,7 +41,6 @@ struct FWingProperty
bool SetInt64(int64 I, WingOut Errors) const;
bool SetBool(bool B, WingOut Errors) const;
bool SetText(FString Value, WingOut Errors) const;
bool SetJson(const FJsonValue &Value, WingOut Errors) const;
// Fetch a value. If an error occurs such as a type
// mismatch, returns an empty optional and prints an
@@ -72,14 +75,24 @@ struct FWingProperty
//
// This gets the properties that are literally present in the
// specified object or struct. No special interpretation is done.
// If mutable is false, all properties will be marked non-editable.
//
static TArray<FWingProperty> GetAll(FWingStructAndUStruct Obj);
static TArray<FWingProperty> GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
// Get all the visible properties of the specified object or struct.
//
// This gets the properties that have CPF_Edit marked on them.
// If mutable is false, all properties will be marked non-editable.
//
static TArray<FWingProperty> GetVisible(FWingStructAndUStruct Obj);
static TArray<FWingProperty> GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
static bool PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors);
// Convenience versions of GetAll and GetVisible for UObjects.
//
static TArray<FWingProperty> GetAll(UObject *Obj, bool Mutable)
{ return GetAll(Obj, Obj, Obj->GetClass(), Mutable); }
static TArray<FWingProperty> GetVisible(UObject *Obj, bool Mutable)
{ return GetVisible(Obj, Obj, Obj->GetClass(), Mutable); }
// Get just the names of the properties of the specified struct/class.
//
@@ -108,18 +121,7 @@ struct FWingProperty
//
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
// Functions to populate properties from a JSON object.
//
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json,
bool AllOptional, WingOut Errors);
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json,
bool AllOptional, WingOut Errors);
// Functions to populate properties from a JSON object.
//
private:
static void StripEditable(TArray<FWingProperty> &Props);
static bool IsUnsigned(FNumericProperty* Prop);
static bool IsPinTypeProperty(FProperty *Prop);
void PrintExpectsReceived(const TCHAR *Type, WingOut Errors) const;

View File

@@ -11,6 +11,7 @@
#include "WingServer.generated.h"
class FSocket;
class FJsonObject;
/**
* UWingServer — editor subsystem that listens on a TCP socket and dispatches
@@ -72,13 +73,14 @@ private:
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
TArray<FWingHandlerConfig> WingHandlerRegistry; // sorted by Name
void BuildWingHandlerRegistry();
void OnModulesChanged(FName ModuleName, EModuleChangeReason Reason);
FDelegateHandle ModulesChangedHandle;
FDelegateHandle LoadingPhasesCompleteHandle;
FWingHandlerConfig* FindHandler(const FString& Name);
// Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line);
void TryCallHandler(const FString &Line);
// Handle a complete request and return the response bytes.
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
void PreCallHandler();
FString PostCallHandler();
void TryCallHandler(TArrayView<const FString> Argv);
// ----- TCP server -----
FSocket* ListenSocket = nullptr;
@@ -95,22 +97,23 @@ private:
TArray<TSharedPtr<FClientConnection>> Clients;
void AcceptNewConnections();
void CleanupFinishedClients();
static uint32 UnpackBigEndian(const uint8 *Data);
static bool DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv);
static void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
static bool ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest);
static bool ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen);
static bool ReceiveRequest(
FSocket* Socket, TArray<uint8>& OutRequest);
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
static bool ProcessRequestOnGameThread(
const FString& Request, FString& Response);
const TArray<uint8>& Request, TArray<uint8>& Response);
static void WaitForAssetRegistry();
// ----- The Critical Section -----
struct FPendingMessage
{
FString Line;
TPromise<FString> Response;
FPendingMessage() : Response(TPromise<FString>()) {}
TArray<uint8> Request;
TPromise<TArray<uint8>> Response;
FPendingMessage() : Response(TPromise<TArray<uint8>>()) {}
};
FCriticalSection Mutex;
TArray<TSharedPtr<FPendingMessage>> PendingMessages;

View File

@@ -57,6 +57,9 @@ public:
// Empty the variable list.
void Empty() { Variables.Empty(); }
// Add a variable.
void Add(const Var &Var) { Variables.Add(Var); }
// Return true if the variables are empty.
bool IsEmpty() { return Variables.IsEmpty(); }
@@ -72,16 +75,6 @@ public:
// Check the sanity of the vars in the array. If allow
// is false, then no variables are allowed in the array.
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
// Parse variables from a string.
bool ParseString(const FString &Input, WingOut Errors);
// Parse variable names only from a string.
bool ParseNamesString(const FString &Input, WingOut Errors);
private:
bool ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
};
class WingVariables
@@ -125,6 +118,10 @@ public:
void Print(WingOut Out);
// Parse variables.
bool Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors);
// Load: clear the workspace, then
// copy everything from the backing store into the workspace.
@@ -193,4 +190,8 @@ private:
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
bool ErrorNoBackingStore(WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors);
WingVariableList *GetList(FName Name);
};

View File

@@ -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()

View File

@@ -1,37 +1,23 @@
#!/usr/bin/env python3
"""
Human-friendly MCP test client.
UE Wingman command-line tool. This tool simply packages up its
argv and sends it to the plugin, and then prints whatever the
plugin sends back. All the real work is done in the plugin.
Usage: ue-wingman.py <command> [key=value ...]
Values starting with '[' or '{' are parsed as JSON.
Usage: ue-wingman.py <arg1> [arg2 ...]
"""
import sys
import json
import socket
import struct
HOST = "localhost"
PORT = 9851
TIMEOUT = 120
TIMEOUT = 15
def main():
args = sys.argv[1:]
if not args:
print("Usage: ue-wingman.py <command> [key=value ...]")
sys.exit(1)
msg = {"command": args[0]}
for arg in args[1:]:
key, _, value = arg.partition("=")
if value and value[0] in ('[', '{'):
try:
value = json.loads(value)
except json.JSONDecodeError as e:
print(f"Bad JSON in {key}: {e.msg}")
sys.exit(1)
msg[key] = value
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT)
@@ -41,7 +27,14 @@ def main():
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
sys.exit(1)
sock.sendall((json.dumps(msg) + "\0").encode())
payload = bytearray()
for arg in args:
data = arg.encode()
payload += struct.pack("!I", len(data))
payload += data
sock.sendall(struct.pack("!I", len(payload)))
sock.sendall(payload)
sock.shutdown(socket.SHUT_WR)
result = b""
while True:
@@ -49,17 +42,9 @@ def main():
if not chunk:
break
result += chunk
if b"\0" in result:
break
sock.close()
result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode()
try:
parsed = json.loads(result)
print(json.dumps(parsed, indent=2))
except json.JSONDecodeError:
print(result)
print(result.decode(), end="")
if __name__ == "__main__":
main()

View File

@@ -1,5 +1,6 @@
#pragma once
#include <string_view>
#include "CoreMinimal.h"
#include "CoreUObject.h"
#include "Containers/Deque.h"

View File

@@ -125,6 +125,21 @@ enum class ElxLuaSyntaxCheck : uint8 {
InvalidLua,
};
////////////////////////////////////////////////////////////
//
// ElxControllerType
//
// The three types of controller recognized by luprex.
//
////////////////////////////////////////////////////////////
UENUM(BlueprintType)
enum class ElxControllerType : uint8 {
KeyboardMouse,
XboxGamepad,
PlayStationGamepad,
};
////////////////////////////////////////////////////////////
//
// Log Categories

View File

@@ -6,6 +6,8 @@
#include "Layout/Geometry.h"
#include "Widgets/Layout/Anchors.h"
#include "Common.h"
#include "Engine/GameViewportClient.h"
#include "Slate/SGameLayerManager.h"
#include "Kismet/KismetTextLibrary.h"
#include "UObject/UObjectIterator.h"
@@ -246,17 +248,32 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataTransform(const FTra
FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataGeometry(const FGeometry &AutoConvertedValue, const FString &Name)
{
FVector2D LocalSize = AutoConvertedValue.GetLocalSize();
FVector2D AbsPos = AutoConvertedValue.GetAbsolutePosition();
FVector2D AbsSize = AutoConvertedValue.GetAbsoluteSize();
FVector2D UL = AutoConvertedValue.GetAbsolutePosition();
FVector2D LR = AutoConvertedValue.GetAbsolutePositionAtCoordinates(FVector2f(1.0f, 1.0f));
if (GEngine && GEngine->GameViewport)
{
TSharedPtr<IGameLayerManager> GameLayerManager = GEngine->GameViewport->GetGameLayerManager();
if (GameLayerManager.IsValid())
{
const FGeometry ViewportGeometry = GameLayerManager->GetViewportWidgetHostGeometry();
const FVector2D ViewportLocalSize = FVector2D(ViewportGeometry.GetLocalSize());
FVector2D ViewportPixelSize;
GEngine->GameViewport->GetViewportSize(ViewportPixelSize);
if (ViewportLocalSize.X > 0.0 && ViewportLocalSize.Y > 0.0)
{
const FVector2D PixelScale = ViewportPixelSize / ViewportLocalSize;
UL = ViewportGeometry.AbsoluteToLocal(UL) * PixelScale;
LR = ViewportGeometry.AbsoluteToLocal(LR) * PixelScale;
}
}
}
FFormatArgumentData Result;
Result.ArgumentValueType = EFormatArgumentType::Text;
Result.ArgumentName = Name;
Result.ArgumentValue = FText::Format(
INVTEXT("Geom(Local={0}x{1} Abs={2}x{3} Pos={4},{5})"),
FText::AsNumber(LocalSize.X), FText::AsNumber(LocalSize.Y),
FText::AsNumber(AbsSize.X), FText::AsNumber(AbsSize.Y),
FText::AsNumber(AbsPos.X), FText::AsNumber(AbsPos.Y));
INVTEXT("UL={0},{1} LR={2},{3}"),
FText::AsNumber(UL.X), FText::AsNumber(UL.Y),
FText::AsNumber(LR.X), FText::AsNumber(LR.Y));
return Result;
}
@@ -300,6 +317,12 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co
return Result;
}
FText UlxFormatDataLibrary::FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
FText InPatternText(FText::FromString(InPattern));
return FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
}
void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
// For throttled verbosity levels, suppress repeated messages with the
@@ -312,7 +335,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
double Now = FPlatformTime::Seconds();
FString Key = Context->GetClass()->GetName() + TEXT("::") + InPattern;
double &Last = LastLogTime.FindOrAdd(Key, 0.0);
if (Now - Last < 1.0)
if (Now - Last < 2.0)
{
return;
}
@@ -321,8 +344,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL
// Generate the formatted string.
//
FText InPatternText(FText::FromString(InPattern));
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
FText Message = FormatMessageInternal(InPattern, MoveTemp(InArgs));
FString MessageString = Message.ToString();
// Get the blueprint name.

View File

@@ -105,6 +105,13 @@ public:
//
static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild);
// Format a message using FTextFormatter::Format.
// Meant to be used internally by the Format Message K2Node,
// which needs an impure wrapper around formatting.
//
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static FText FormatMessageInternal(const FString &InPattern, TArray<FFormatArgumentData> InArgs);
// Format a message using FTextFormatter::Format, and send
// it to UE_LOG. The Context object's name is used as the
// log category. Meant to be used internally by the Format

View File

@@ -250,7 +250,7 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
}
else
{
FormatFunction = UKismetTextLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UKismetTextLibrary, Format));
FormatFunction = UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatMessageInternal));
}
// This is the node that does all the Format work and outputs the message.

View File

@@ -0,0 +1,105 @@
#include "InputDeviceTracker.h"
#include "Framework/Application/SlateApplication.h"
#include "GenericPlatform/GenericApplicationMessageHandler.h"
#include "InputCoreTypes.h"
// Last observed device classification. Read with no
// synchronization; updates happen on the game thread
// from Slate's input pipeline, and stale reads on
// other threads are acceptable for this use case.
//
static ElxControllerType GLastControllerType = ElxControllerType::KeyboardMouse;
// Keywords identifying a PlayStation-family gamepad.
// Matched case-insensitively against the InputDeviceName
// and HardwareDeviceIdentifier fields of the current
// FInputDeviceScope.
//
static const TCHAR* const PlaystationKeywords[] = {
TEXT("Playstation"),
TEXT("PS3"),
TEXT("PS4"),
TEXT("PS5"),
TEXT("PS6"),
TEXT("PS7"),
TEXT("Dualsense"),
TEXT("Dualshock"),
};
namespace
{
bool ContainsAnyPlaystationKeyword(const FString& Haystack)
{
for (const TCHAR* Keyword : PlaystationKeywords)
{
if (Haystack.Contains(Keyword, ESearchCase::IgnoreCase))
{
return true;
}
}
return false;
}
// Classifies the active gamepad by scanning the current
// FInputDeviceScope. Defaults to Xbox; switches to
// PlayStation only on a keyword match.
//
ElxControllerType ClassifyGamepadFromScope()
{
const FInputDeviceScope* Scope = FInputDeviceScope::GetCurrent();
if (Scope != nullptr)
{
if (ContainsAnyPlaystationKeyword(Scope->InputDeviceName.ToString()) ||
ContainsAnyPlaystationKeyword(Scope->HardwareDeviceIdentifier))
{
return ElxControllerType::PlayStationGamepad;
}
}
return ElxControllerType::XboxGamepad;
}
}
bool FInputDeviceTrackerProcessor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent)
{
if (KeyEvent.GetKey().IsGamepadKey())
{
GLastControllerType = ClassifyGamepadFromScope();
}
else
{
GLastControllerType = ElxControllerType::KeyboardMouse;
}
return false;
}
bool FInputDeviceTrackerProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
GLastControllerType = ElxControllerType::KeyboardMouse;
return false;
}
void UlxInputDeviceTracker::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
if (FSlateApplication::IsInitialized())
{
Processor = MakeShared<FInputDeviceTrackerProcessor>();
FSlateApplication::Get().RegisterInputPreProcessor(Processor);
}
}
void UlxInputDeviceTracker::Deinitialize()
{
if (Processor.IsValid() && FSlateApplication::IsInitialized())
{
FSlateApplication::Get().UnregisterInputPreProcessor(Processor);
}
Processor.Reset();
Super::Deinitialize();
}
ElxControllerType UlxInputDeviceTracker::GetLastControllerType()
{
return GLastControllerType;
}

View File

@@ -0,0 +1,64 @@
////////////////////////////////////////////////////////////
//
// InputDeviceTracker.h
//
// Tracks the most recently used input device, classifying
// it as keyboard/mouse, Xbox gamepad, or PlayStation
// gamepad. The subsystem registers a Slate input
// preprocessor that watches button-down events; analog
// and mouse-move events are ignored. Read the current
// classification via the static accessor.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Framework/Application/IInputProcessor.h"
#include "Common.h"
#include "InputDeviceTracker.generated.h"
////////////////////////////////////////////////////////////
//
// FInputDeviceTrackerProcessor
//
// Slate input preprocessor. Updates the device-class
// static on each button-down event. Never consumes
// events (always returns false).
//
////////////////////////////////////////////////////////////
class FInputDeviceTrackerProcessor : public IInputProcessor
{
public:
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override {}
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& KeyEvent) override;
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
};
////////////////////////////////////////////////////////////
//
// UlxInputDeviceTracker
//
////////////////////////////////////////////////////////////
UCLASS(MinimalAPI)
class UlxInputDeviceTracker : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Returns the classification of the most recently used
// input device. Defaults to KeyboardMouse until the
// first gamepad button event is observed.
//
UFUNCTION(BlueprintCallable, Category="Luprex|Input")
static ElxControllerType GetLastControllerType();
private:
TSharedPtr<FInputDeviceTrackerProcessor> Processor;
};

View File

@@ -1,112 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputEvents.cpp
//
////////////////////////////////////////////////////////////
#include "InputEvents.h"
#include "Common.h"
bool FlxEventRequest::operator==(const FlxEventRequest &Other) const
{
return (Widget == Other.Widget) &&
(UseUIOnly == Other.UseUIOnly) &&
(ShowPointer == Other.ShowPointer) &&
(Hotkeys == Other.Hotkeys);
}
bool FlxEventRequests::SanityCheck(const FlxEventRequest &Request)
{
if (Request.Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
return false;
}
if (Request.ShowPointer && !Request.UseUIOnly)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: ShowPointer requires UseUIOnly."));
return false;
}
if (Request.UseUIOnly && !Request.Hotkeys.IsEmpty())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: Widget asked for all events, and also, specific events"));
return false;
}
return true;
}
void FlxEventRequests::SplitHighLow(View &High, View &Low)
{
int32 NumHigh = 0;
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++;
int32 NumLow = Requests.Num() - NumHigh;
High = View(Requests.GetData(), NumHigh);
Low = View(Requests.GetData() + NumHigh, NumLow);
}
void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
{
// Divide the array into a high-priority slice and a low-priority slice.
View High, Low;
SplitHighLow(High, Low);
// This is a simple test to see if anything is going to change.
// If not, we return early and avoid setting the dirty bit.
if (NewRequest.UseUIOnly)
{
if ((High.Num() > 0) && (High[0] == NewRequest)) return;
}
else
{
if ((Low.Num() > 0) && (Low[0] == NewRequest)) return;
}
// We're going to build a new version of the requests array.
TArray<FlxEventRequest> Updated;
// Add all high priority requests to the updated array, new request first.
if (NewRequest.UseUIOnly) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : High)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
// Add all low priority requests to the updated array, new request first.
if (!NewRequest.UseUIOnly) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : Low)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
Swap(Requests, Updated);
Dirty = true;
}
void FlxEventRequests::Remove(UUserWidget *Widget)
{
int32 N = Requests.Num();
Requests.RemoveAll([Widget](const FlxEventRequest &Entry)
{
return Entry.Widget == Widget;
});
if (Requests.Num() < N) Dirty = true;
}
void FlxEventRequests::GarbageCollect()
{
int32 N = Requests.Num();
Requests.RemoveAll([](const FlxEventRequest &Entry)
{
UUserWidget *W = Entry.Widget;
return W == nullptr || !IsValid(W) || W->GetParent() == nullptr;
});
if (Requests.Num() < N) Dirty = true;
}
FlxEventRequests::InputMode FlxEventRequests::GetRequestedMode() const
{
if ((Requests.Num() > 0) && (Requests[0].UseUIOnly))
{
return InputMode::UIOnly;
}
else
{
return InputMode::GameOnly;
}
}

View File

@@ -1,106 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputEvents.h
//
// Custom input event dispatching system. Uses Unreal's
// built-in input modes (GameOnly / UIOnly) with an
// enhanced input component for GameOnly mode hotkeys.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Blueprint/UserWidget.h"
#include "InputEvents.generated.h"
////////////////////////////////////////////////////////////
//
// FlxEventRequest
//
// A widget's declaration of interest in input events.
//
// Widget: The widget that wants to receive events.
// UseUIOnly: If true, activating this request puts
// the system into UIOnly mode.
// ShowPointer: If true, the mouse pointer should be
// visible when this widget has control.
// Hotkeys: Keys that go to this widget when the
// player is in GameOnly mode.
//
////////////////////////////////////////////////////////////
USTRUCT(BlueprintType)
struct FlxEventRequest
{
GENERATED_BODY()
FlxEventRequest() = default;
FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray<FKey> &InHotkeys)
: Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {}
bool operator == (const FlxEventRequest &Other) const;
UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite)
bool UseUIOnly = false;
UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite)
TArray<FKey> Hotkeys;
};
USTRUCT()
struct FlxEventRequests
{
GENERATED_BODY()
private:
UPROPERTY()
// High priority requests are always before low-priority.
// Otherwise, these are in order of most recent first.
TArray<FlxEventRequest> Requests;
UPROPERTY()
bool Dirty = true;
public:
enum class InputMode { UIOnly, GameOnly };
using View = TArrayView<FlxEventRequest>;
// Get the requests array.
const TArray<FlxEventRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxEventRequest &Request);
// Divide Requests into a high-priority slice and a low-priority slice.
void SplitHighLow(View &High, View &Low);
// Apply a request. Replaces any previous request by the same widget.
void Request(const FlxEventRequest &NewRequest);
// Remove all requests by the specified widget.
void Remove(UUserWidget *Widget);
// Remove any requests by dead widgets or widgets with no parents.
void GarbageCollect();
// Return true if the requests have changed since the last ClearDirty.
bool IsDirty() { return Dirty; }
// Clear the dirty flag.
void ClearDirty() { Dirty = false; }
// Set the dirty flag.
void SetDirty() { Dirty = true; }
// Get the currently-requested mode.
InputMode GetRequestedMode() const;
};

View File

@@ -31,6 +31,7 @@ public class Integration : ModuleRules
"UMG",
"UMGEditor",
"EditorSubsystem",
"ApplicationCore",
});
PrivateDependencyModuleNames.Add("Slate");

View File

@@ -73,7 +73,6 @@ void ALuprexGameModeBase::UpdateConsoleOutput() {
}
}
#pragma optimize("", off)
void ALuprexGameModeBase::UpdateTangibles() {
double radius = 1000.0; // Hardwired for now.
using TanArray = UlxTangibleManager::TanArray;
@@ -132,8 +131,9 @@ void ALuprexGameModeBase::UpdatePossessedTangible() {
void ALuprexGameModeBase::UpdateLuaSourceCode() {
FlxLockedWrapper lockedwrap;
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()))
if (lockedwrap->get_rescan_lua_source(lockedwrap.Get()) || ReloadSource)
{
ReloadSource = false;
drvutil::ostringstream srcpak;
FString LuprexRoot = FPaths::Combine(FPaths::ProjectDir(), TEXT("luprex"));
std::string srcpakerr = drvutil::package_lua_source(TCHAR_TO_UTF8(*LuprexRoot), &srcpak);
@@ -172,7 +172,7 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
if (PC != nullptr)
{
PC->UpdateLookAt();
PC->UpdateEventDispatch();
PC->UpdateInputMode();
}
}
}
@@ -259,4 +259,9 @@ ALuprexGameModeBase *ALuprexGameModeBase::FromContext(const UObject *context) {
return result;
}
void ALuprexGameModeBase::TriggerReloadSource(const UObject *WorldContextObject) {
ALuprexGameModeBase *GameMode = FromContext(WorldContextObject);
GameMode->ReloadSource = true;
}

View File

@@ -63,6 +63,11 @@ public:
// Get the current Luprex Game Mode Base, given a Context object.
static ALuprexGameModeBase *FromContext(const UObject *Context);
// Set the ReloadSource flag on the current Luprex game mode, causing
// the Lua source to be reloaded on the next tick.
UFUNCTION(BlueprintCallable, Category = "Luprex|Miscellaneous", meta = (WorldContext = "WorldContextObject"))
static void TriggerReloadSource(const UObject *WorldContextObject);
// The sensitivity level at which a log message triggers a debugger breakpoint.
UPROPERTY(EditAnywhere, Category="Debugging Tools")
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
@@ -76,6 +81,9 @@ public:
// This is always true unless you use the debugger to set it to false.
bool TickEnabled = true;
// True to trigger a source reload.
bool ReloadSource = false;
// Current Player ID
int64 PlayerId = 0;

View File

@@ -28,7 +28,7 @@ void UlxUserWidget::BackupInputComponent()
}
}
void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction)
void UlxUserWidget::DisableInputAction(const UInputAction* InputAction)
{
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return;
@@ -40,9 +40,9 @@ void UlxUserWidget::DisableEventBinding(const UInputAction* InputAction)
});
}
void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction)
void UlxUserWidget::RestoreInputAction(const UInputAction* InputAction)
{
DisableEventBinding(InputAction);
DisableInputAction(InputAction);
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return;
@@ -59,7 +59,7 @@ void UlxUserWidget::RestoreInputBinding(const UInputAction* InputAction)
void UlxUserWidget::RedirectInputAction(const UInputAction* From, const UInputAction* To)
{
DisableEventBinding(From);
DisableInputAction(From);
UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
if (!EIC) return;

View File

@@ -22,20 +22,18 @@ public:
// from the component and reinstated without losing their delegates.
void BackupInputComponent();
// Remove every live event binding whose action is InputAction.
// No-op if there are none, or if InputComponent isn't enhanced.
// Removes all handlers for 'InputAction'. That includes temporarily
// deactivating event graph nodes that handle 'InputAction'.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void DisableEventBinding(const UInputAction* InputAction);
void DisableInputAction(const UInputAction* InputAction);
// Replace any live bindings for InputAction with fresh clones of every
// saved binding for that action. Leaves the backup array intact so this
// can be called repeatedly.
// Reactivates any graph nodes that handle 'InputAction', and
// removes any other handlers for 'InputAction'.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void RestoreInputBinding(const UInputAction* InputAction);
void RestoreInputAction(const UInputAction* InputAction);
// Install live bindings on From that, when fired, dispatch through a
// clone of each saved binding for To. Clears any pre-existing live
// bindings on From first. Backup array is untouched.
// Any event graph nodes that handle 'to' are made to also
// handle 'From' events. Any other handlers of 'From' are removed.
UFUNCTION(BlueprintCallable, Category="Luprex|Widget Enhanced Input")
void RedirectInputAction(const UInputAction* From, const UInputAction* To);

View File

@@ -0,0 +1,63 @@
////////////////////////////////////////////////////////////
//
// LuprexViewportClient.cpp
//
////////////////////////////////////////////////////////////
#include "LuprexViewportClient.h"
#include "Common.h"
#include "PlayerControllerBase.h"
#include "RootCanvas.h"
#include "Engine/GameInstance.h"
#include "Framework/Application/SlateApplication.h"
#include "Layout/WidgetPath.h"
#include "Slate/SObjectWidget.h"
UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer)
: Super(ObjectInitializer)
{
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient constructed"));
}
bool UlxViewportClient::TryBringToFront(const FWidgetPath &Path)
{
UGameInstance *GI = GetGameInstance();
if (!GI) return false;
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(
GI->GetFirstLocalPlayerController(GetWorld()));
if (!PC) return false;
for (int32 Idx = 0; Idx < Path.Widgets.Num(); ++Idx)
{
SWidget &SW = Path.Widgets[Idx].Widget.Get();
if (SW.GetType() != FName(TEXT("SObjectWidget"))) continue;
UUserWidget *Widget = static_cast<SObjectWidget&>(SW).GetWidgetObject();
if (Widget && Widget->GetParent() == PC->RootCanvas)
{
UlxRootCanvasPanel::BringToFront(Widget);
return true;
}
}
return false;
}
bool UlxViewportClient::InputKey(const FInputKeyEventArgs &EventArgs)
{
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient::InputKey key=%s event=%d"),
*EventArgs.Key.ToString(), (int32)EventArgs.Event);
// Only act on left mouse button presses that bubbled up to the
// viewport unhandled. If the click landed on a descendant of a
// top-level widget in the root canvas, bring that top-level widget
// to the front.
if ((EventArgs.Event == IE_Pressed) && (EventArgs.Key == EKeys::LeftMouseButton))
{
FSlateApplication &Slate = FSlateApplication::Get();
FVector2D MousePos = Slate.GetCursorPos();
FWidgetPath Path = Slate.LocateWindowUnderMouse(
MousePos, Slate.GetInteractiveTopLevelWindows());
if (Path.IsValid() && TryBringToFront(Path)) return true;
}
return Super::InputKey(EventArgs);
}

View File

@@ -0,0 +1,32 @@
////////////////////////////////////////////////////////////
//
// LuprexViewportClient.h
//
// Custom game viewport client. Implements a project-wide
// click-to-focus rule: when a left-mouse-button click is
// not handled by any widget and bubbles up to the viewport,
// we hit-test under the cursor and focus the nearest
// focusable ancestor of whatever was hit.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameViewportClient.h"
#include "Layout/WidgetPath.h"
#include "LuprexViewportClient.generated.h"
UCLASS()
class INTEGRATION_API UlxViewportClient : public UGameViewportClient
{
GENERATED_BODY()
public:
UlxViewportClient(const FObjectInitializer &ObjectInitializer);
virtual bool InputKey(const FInputKeyEventArgs &EventArgs) override;
private:
bool TryBringToFront(const FWidgetPath &Path);
};

View File

@@ -1,27 +1,21 @@
#include "PlayerControllerBase.h"
#include "Common.h"
#include "GameFramework/InputDeviceSubsystem.h"
#include "GameFramework/InputSettings.h"
#include "RootCanvas.h"
#include "Tangible.h"
#include "TangibleManager.h"
#include "Kismet/GameplayStatics.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/InputComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Engine/LevelScriptActor.h"
#include "Engine/LocalPlayer.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Application/SlateUser.h"
#include "Kismet/GameplayStatics.h"
#include "Widgets/SViewport.h"
#include "Slate/SObjectWidget.h"
FString AlxPlayerControllerBase::GetUserWidgetName(SWidget *W)
{
while (W)
{
if (W->GetType() == FName("SObjectWidget"))
{
UUserWidget *UW = static_cast<SObjectWidget*>(W)->GetWidgetObject();
if (UW) return UW->GetClass()->GetName();
}
W = W->GetParentWidget().Get();
}
return TEXT("Unknown Widget");
}
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
{
@@ -73,92 +67,257 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
void AlxPlayerControllerBase::BeginPlay()
{
// Build the root UMG stack BEFORE Super::BeginPlay. Super calls
// ReceiveBeginPlay, which fires the Blueprint Event BeginPlay;
// BP code there may immediately try to add widgets to RootCanvas,
// so the canvas must already exist.
RootWidget = CreateWidget<UlxRootWidget>(this);
RootCanvas = RootWidget->WidgetTree->ConstructWidget<UlxRootCanvasPanel>();
RootWidget->WidgetTree->RootWidget = RootCanvas;
RootWidget->AddToViewport(0);
Super::BeginPlay();
HotkeyInputComponent = NewObject<UInputComponent>(this);
HotkeyInputComponent->bBlockInput = false;
PushInputComponent(HotkeyInputComponent);
if (FSlateApplication::IsInitialized())
{
FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject(
this, &AlxPlayerControllerBase::HandleFocusChanging);
}
}
void AlxPlayerControllerBase::UpdateEventDispatch()
void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
EventRequests.GarbageCollect();
// If we're in GameOnly mode, check that focus is still on the viewport.
if (CurrentInputMode == InputMode::GameOnly)
if (FocusChangingHandle.IsValid() && FSlateApplication::IsInitialized())
{
UGameViewportClient *GVC = GetWorld() ? GetWorld()->GetGameViewport() : nullptr;
if (GVC)
{
TSharedPtr<SViewport> ViewportWidget = GVC->GetGameViewportWidget();
if (ViewportWidget.IsValid())
{
TSharedPtr<SWidget> Focused = FSlateApplication::Get().GetKeyboardFocusedWidget();
if (Focused.Get() != ViewportWidget.Get())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, keyboard focus must stay on viewport, but was stolen by: %s. Restoring."), *GetUserWidgetName(Focused.Get()));
EventRequests.SetDirty();
FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle);
FocusChangingHandle.Reset();
}
if (!ViewportWidget->HasMouseCapture())
if (IsValid(RootWidget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, viewport must have mouse capture, but lost it. Restoring."));
EventRequests.SetDirty();
RootWidget->RemoveFromParent();
}
RootWidget = nullptr;
RootCanvas = nullptr;
Super::EndPlay(EndPlayReason);
}
void AlxPlayerControllerBase::HandleFocusChanging(
const FFocusEvent &FocusEvent,
const FWeakWidgetPath &OldPath,
const TSharedPtr<SWidget> &OldFocusedWidget,
const FWidgetPath &NewPath,
const TSharedPtr<SWidget> &NewFocusedWidget)
{
UE_LOG(LogLuprexIntegration, Display,
TEXT("Focus changing: '%s' -> '%s' (cause: %s)"),
OldFocusedWidget.IsValid() ? *OldFocusedWidget->GetTypeAsString() : TEXT("<none>"),
NewFocusedWidget.IsValid() ? *NewFocusedWidget->GetTypeAsString() : TEXT("<none>"),
*UEnum::GetValueAsString(FocusEvent.GetCause()));
}
UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *Widget)
{
if (!IsValid(Widget)) return nullptr;
// Cache the FProperty on first call. FProperties are owned by
// the native UUserWidget UClass, which lives for the process
// lifetime, so the pointer stays valid without us needing to
// root anything against GC. Static local init is thread-safe.
static FObjectProperty *InputComponentProp = FindFProperty<FObjectProperty>(
UUserWidget::StaticClass(), TEXT("InputComponent"));
check(InputComponentProp);
UObject *Value = InputComponentProp->GetObjectPropertyValue_InContainer(Widget);
return Cast<UInputComponent>(Value);
}
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
{
if (!IsValid(Widget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("AddWidgetToRoot called with an invalid widget."));
return;
}
APlayerController *OwningPC = Widget->GetOwningPlayer();
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(OwningPC);
if (PC == nullptr)
{
UE_LOG(LogLuprexIntegration, Error,
TEXT("AddWidgetToRoot: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
*Widget->GetName(), *GetNameSafe(OwningPC));
return;
}
if (PC->RootCanvas == nullptr)
{
UE_LOG(LogLuprexIntegration, Error,
TEXT("AddWidgetToRoot: root canvas is not initialized, this is probably an initialization order issue"));
return;
}
if (Widget->GetParent() == PC->RootCanvas) return;
PC->RootCanvas->AddChildToRootCanvas(Widget);
}
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
{
// Controlled pawn gets last dibs on the input stack
APawn* ControlledPawn = GetPawnOrSpectator();
if (ControlledPawn)
{
if (ControlledPawn->InputEnabled())
{
// Get the explicit input component that is created upon Pawn possession. This one gets last dibs.
if (ControlledPawn->InputComponent)
{
InputStack.Push(ControlledPawn->InputComponent);
}
// See if there is another InputComponent that was added to the Pawn's components array (possibly by script).
for (UActorComponent* ActorComponent : ControlledPawn->GetComponents())
{
UInputComponent* PawnInputComponent = Cast<UInputComponent>(ActorComponent);
if (PawnInputComponent && PawnInputComponent != ControlledPawn->InputComponent)
{
InputStack.Push(PawnInputComponent);
}
}
}
}
if (!EventRequests.IsDirty()) return;
EventRequests.ClearDirty();
CurrentInputMode = EventRequests.GetRequestedMode();
const TArray<FlxEventRequest> &Requests = EventRequests.GetRequests();
if (CurrentInputMode == InputMode::UIOnly)
// LevelScriptActors are put on the stack next
for (ULevel* Level : GetWorld()->GetLevels())
{
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget()));
ALevelScriptActor* ScriptActor = Level->GetLevelScriptActor();
if (ScriptActor)
{
if (ScriptActor->InputEnabled() && ScriptActor->InputComponent)
{
InputStack.Push(ScriptActor->InputComponent);
}
}
}
if (InputEnabled())
{
InputStack.Push(InputComponent);
}
// Get the widget slots.
TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
// Generate a set of input components that are being managed by the WidgetSlots.
TSet<UInputComponent*> WidgetManagedComponents;
for (UlxRootCanvasSlot *Slot : WidgetSlots)
{
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
UInputComponent *IC = GetWidgetInputComponent(Widget);
if (IC) WidgetManagedComponents.Add(IC);
}
// Add components in the CurrentInputStack, *unless* they are being managed
// by widgets. If they're being managed by widgets, they get added later.
for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx)
{
UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC))
{
if (!WidgetManagedComponents.Contains(IC)) InputStack.Push(IC);
}
else
{
SetInputMode(FInputModeGameOnly());
HotkeyInputComponent->KeyBindings.Empty();
TSet<FKey> BoundKeys;
for (const FlxEventRequest &Req : Requests)
{
for (const FKey &Key : Req.Hotkeys)
{
if (!BoundKeys.Contains(Key))
{
BoundKeys.Add(Key);
HotkeyInputComponent->BindKey(Key, IE_Pressed, this, &AlxPlayerControllerBase::ForwardKeyEvent);
CurrentInputStack.RemoveAt(Idx--);
}
}
// Now add the widget-managed input components.
for (UlxRootCanvasSlot *Slot : WidgetSlots)
{
if (Slot->EnableEnhancedInput)
{
UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
UInputComponent *IC = GetWidgetInputComponent(Widget);
if (IC)
{
IC->bBlockInput = Slot->BlockInput;
InputStack.Push(IC);
}
}
}
}
void AlxPlayerControllerBase::ForwardKeyEvent(FKey Key)
void AlxPlayerControllerBase::UpdateInputMode()
{
// TODO: implement
}
// Get all the various objects we need to be able to manipulate
// the input mode.
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
ULocalPlayer *LocalPlayer = Cast<ULocalPlayer>(Player);
if (GameViewportClient == nullptr || LocalPlayer == nullptr) return;
TSharedPtr<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget();
if (!ViewportWidget.IsValid()) return;
TSharedRef<SViewport> ViewportWidgetRef = ViewportWidget.ToSharedRef();
FReply &SlateOperations = LocalPlayer->GetSlateOperations();
TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser();
if (!SlateUser.IsValid()) return;
void AlxPlayerControllerBase::RequestEvents(const FlxEventRequest &Request)
{
if (!FlxEventRequests::SanityCheck(Request)) return;
AlxPlayerControllerBase *PC = FromContext(Request.Widget);
PC->EventRequests.Request(Request);
}
RootCanvas->UpdateZOrders();
void AlxPlayerControllerBase::UnRequestEvents(UUserWidget *Widget)
{
if (Widget == nullptr)
// Get the desired configuration from the top widget.
UUserWidget *Widget = nullptr;
UWidget *Focus = nullptr;
bool ShowPointer = false;
if (UlxRootCanvasSlot *Top = RootCanvas->GetTopWidget())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("UnRequestEvents called with null widget."));
return;
Widget = Cast<UUserWidget>(Top->GetContent());
Focus = Widget->GetDesiredFocusWidget();
ShowPointer = Top->ShowPointer;
}
SetShowMouseCursor(ShowPointer);
if (ShowPointer)
{
// Only release capture if the viewport is currently holding it
// (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture
// every tick would yank capture away from widgets mid-gesture
// (scrollbar drags, slider drags, etc.).
if (SlateUser->DoesWidgetHaveAnyCapture(ViewportWidget))
{
SlateOperations.ReleaseMouseCapture();
}
SlateOperations.ReleaseMouseLock();
GameViewportClient->SetMouseLockMode(EMouseLockMode::DoNotLock);
GameViewportClient->SetHideCursorDuringCapture(false);
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CaptureDuringMouseDown);
}
else
{
// Also captures the mouse to the viewport.
SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
SlateOperations.LockMouseToWidget(ViewportWidgetRef);
GameViewportClient->SetMouseLockMode(EMouseLockMode::LockOnCapture);
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CapturePermanently);
}
GameViewportClient->SetIgnoreInput(false);
// We always put keyboard focus on whatever user widget is in
// front. If the front widget doesn't want keyboard focus,
// then we put keyboard focus on the viewport.
if (Focus)
{
if (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
{
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
}
}
else
{
SlateOperations.SetUserFocus(ViewportWidgetRef);
}
AlxPlayerControllerBase *PC = FromContext(Widget);
PC->EventRequests.Remove(Widget);
}
void AlxPlayerControllerBase::UpdateLookAt()

View File

@@ -1,19 +1,21 @@
#pragma once
#include "CoreMinimal.h"
#include "Common.h"
#include "Engine/HitResult.h"
#include "GameFramework/PlayerController.h"
#include "InputEvents.h"
#include "UObject/ObjectKey.h"
#include "PlayerControllerBase.generated.h"
class UlxRootCanvasPanel;
class UWidget;
UCLASS(BlueprintType, Blueprintable)
class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
{
GENERATED_BODY()
public:
using InputMode = FlxEventRequests::InputMode;
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
@@ -29,12 +31,6 @@ public:
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAtChanged(const UObject *Context);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void RequestEvents(const FlxEventRequest &Request);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void UnRequestEvents(UUserWidget *Widget);
// Blueprint events
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void CalculateLookAt();
@@ -42,19 +38,38 @@ public:
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void LookAtChanged();
virtual void BeginPlay() override;
// Called by GameMode each tick.
void UpdateLookAt();
// Rebuild input component and switch input mode.
void UpdateEventDispatch();
// Called by GameMode each tick. GCs dead requests and will
// eventually reconcile focus, pointer, and capture state.
void UpdateInputMode();
// Handler for GameOnly mode hotkey presses.
void ForwardKeyEvent(FKey Key);
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// Walk up from a Slate widget to find the nearest UMG widget class name.
static FString GetUserWidgetName(SWidget *W);
private:
FDelegateHandle FocusChangingHandle;
void HandleFocusChanging(
const struct FFocusEvent &FocusEvent,
const class FWeakWidgetPath &OldPath,
const TSharedPtr<class SWidget> &OldFocusedWidget,
const class FWidgetPath &NewPath,
const TSharedPtr<class SWidget> &NewFocusedWidget);
public:
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
// Read UUserWidget::InputComponent via reflection. The field is
// protected and has no public accessor; this reaches through the
// FProperty so we always see the current value without caching it.
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
// Add a widget to the root canvas at ZOrder 0 with default slot flags.
UFUNCTION(BlueprintCallable, Category = "Luprex|Root Canvas",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
static void AddWidgetToRoot(class UUserWidget *Widget);
// Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context);
@@ -62,15 +77,16 @@ public:
UPROPERTY()
FHitResult CurrentLookAt;
// The single top-level UUserWidget added to the viewport. All
// top-level UI widgets are children of RootCanvas inside it.
UPROPERTY()
FlxEventRequests EventRequests;
UUserWidget *RootWidget = nullptr;
// Input component for GameOnly mode: catches hotkeys only.
// The root canvas panel inside RootWidget. Children of this
// canvas are the top-level widgets; their slots carry both
// layout and input-mode configuration.
UPROPERTY()
UInputComponent *HotkeyInputComponent = nullptr;
// Current input mode.
InputMode CurrentInputMode = InputMode::GameOnly;
UlxRootCanvasPanel *RootCanvas = nullptr;
bool MustCallLookAtChanged = false;
};

View File

@@ -0,0 +1,251 @@
#include "PromptWidget.h"
#include "InputDeviceTracker.h"
#include "UtilityLibrary.h"
#include "PlayerControllerBase.h"
#include "InputAction.h"
#include "InputMappingContext.h"
#include "Widgets/SOverlay.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SScaleBox.h"
//
// Atlas contents:
//
// Row 1: Keyboard Button no Glyph, Left Mouse, Middle Mouse, Right Mouse
// Row 2: Left Trigger, Right Trigger, Left Shoulder, Right Shoulder
// Row 3: XBFace Down, XBFace Left, XBFace Up, XBFace Right
// Row 4: PSFace Down, PSFace Left, PSFace Up, PSFace Right
// Row 5: Arrow Down, Arrow Left, Arrow Up, Arrow Right
// Row 6: DPad Down, DPad Left, DPad Up, DPad Right
// Row 7: Space Bar, unused, unused, unused
// Row 8: unused, unused, unused, unused.
int32 UlxPromptWidget::ChooseIcon(bool Playstation, FKey Key) const
{
// Mouse buttons (row 1)
if (Key == EKeys::LeftMouseButton) return 1;
if (Key == EKeys::MiddleMouseButton) return 2;
if (Key == EKeys::RightMouseButton) return 3;
// Gamepad triggers and shoulders (row 2)
if (Key == EKeys::Gamepad_LeftTrigger) return 4;
if (Key == EKeys::Gamepad_RightTrigger) return 5;
if (Key == EKeys::Gamepad_LeftShoulder) return 6;
if (Key == EKeys::Gamepad_RightShoulder) return 7;
// Gamepad Face buttons (rows 3 and 4)
if (Key == EKeys::Gamepad_FaceButton_Bottom) return Playstation ? 12 : 8;
if (Key == EKeys::Gamepad_FaceButton_Left) return Playstation ? 13 : 9;
if (Key == EKeys::Gamepad_FaceButton_Top) return Playstation ? 14 : 10;
if (Key == EKeys::Gamepad_FaceButton_Right) return Playstation ? 15 : 11;
// Arrow keys (row 5)
if (Key == EKeys::Down) return 16;
if (Key == EKeys::Left) return 17;
if (Key == EKeys::Up) return 18;
if (Key == EKeys::Right) return 19;
// Gamepad D-Pad (row 6)
if (Key == EKeys::Gamepad_DPad_Down) return 20;
if (Key == EKeys::Gamepad_DPad_Left) return 21;
if (Key == EKeys::Gamepad_DPad_Up) return 22;
if (Key == EKeys::Gamepad_DPad_Right) return 23;
// Space bar (row 7)
if (Key == EKeys::SpaceBar) return 24;
// No icon for this key. Use the blank icon with a label.
return 0;
}
void UlxPromptWidget::ChooseAppearance(int32 &OutIcon, FString &OutGlyph)
{
FKey Key = (ControllerType == ElxControllerType::KeyboardMouse) ? KeyboardKey : GamepadKey;
OutIcon = ChooseIcon(ControllerType == ElxControllerType::PlayStationGamepad, Key);
if (OutIcon == 0) OutGlyph = Key.GetDisplayName().ToString();
else OutGlyph = TEXT("");
}
FBox2f UlxPromptWidget::GetIconUVs(int32 IconIndex)
{
const float ColWidth = 1.0f / 4.0f;
const float RowHeight = 1.0f / 8.0f;
int32 Col = IconIndex % 4;
int32 Row = IconIndex / 4;
float U = Col * ColWidth;
float V = Row * RowHeight;
return FBox2f(FVector2f(U, V), FVector2f(U + ColWidth, V + RowHeight));
}
FMargin UlxPromptWidget::GetScaledMargins() const
{
return FMargin(
GlyphMargins.Left * Size.X,
GlyphMargins.Top * Size.Y,
GlyphMargins.Right * Size.X,
GlyphMargins.Bottom * Size.Y);
}
void UlxPromptWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
if (!MyImage.IsValid()) return;
int32 Icon = 0;
FString Glyph;
ChooseAppearance(Icon, Glyph);
MyBrush.SetUVRegion(GetIconUVs(Icon));
MyImage->InvalidateImage();
MyBox->SetWidthOverride(Size.X);
MyBox->SetHeightOverride(Size.Y);
const float Extra = Depressed ? 0.05f : 0.0f;
const FMargin ExtraPadding(Extra * Size.X, Extra * Size.Y, Extra * Size.X, Extra * Size.Y);
MyImageSlot->SetPadding(ExtraPadding);
MyGlyphSlot->SetPadding(GetScaledMargins() + ExtraPadding);
MyGlyph->SetColorAndOpacity(GlyphColor);
if (!Glyph.IsEmpty())
{
MyGlyph->SetText(FText::FromString(Glyph));
MyGlyph->SetVisibility(EVisibility::HitTestInvisible);
}
else
{
MyGlyph->SetVisibility(EVisibility::Collapsed);
}
}
void UlxPromptWidget::SetSize(FVector2D InSize)
{
if (Size != InSize)
{
Size = InSize;
SynchronizeProperties();
}
}
void UlxPromptWidget::SetGlyphMargins(FMargin InMargins)
{
if (GlyphMargins != InMargins)
{
GlyphMargins = InMargins;
SynchronizeProperties();
}
}
void UlxPromptWidget::SetGlyphColor(FLinearColor InColor)
{
if (GlyphColor != InColor)
{
GlyphColor = InColor;
SynchronizeProperties();
}
}
void UlxPromptWidget::SetDepressed(bool InDepressed)
{
if (Depressed != InDepressed)
{
Depressed = InDepressed;
SynchronizeProperties();
}
}
void UlxPromptWidget::SetKeys(FKey InGamepadKey, FKey InKeyboardKey)
{
if ((GamepadKey != InGamepadKey) || (KeyboardKey != InKeyboardKey))
{
GamepadKey = InGamepadKey;
KeyboardKey = InKeyboardKey;
SynchronizeProperties();
}
}
void UlxPromptWidget::SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction)
{
check(InputMappingContext);
check(EnhancedInputAction);
FKey NewFirstKey;
FKey NewGamepadKey;
FKey NewKeyboardKey;
for (const FEnhancedActionKeyMapping& Mapping : InputMappingContext->GetMappings())
{
if (Mapping.Action != EnhancedInputAction) continue;
FKey Key = Mapping.Key;
if (Key.IsDigital())
{
if (!NewFirstKey.IsValid()) NewFirstKey = Mapping.Key;
if (Key.IsTouch()) { /* not supported */ }
else if (Key.IsGesture()) { /* not supported */ }
else if (Key.IsGamepadKey()) { if (!NewGamepadKey.IsValid()) NewGamepadKey = Key; }
else { if (!NewKeyboardKey.IsValid()) NewKeyboardKey = Key; }
}
}
if (!NewGamepadKey.IsValid()) NewGamepadKey = NewFirstKey;
if (!NewKeyboardKey.IsValid()) NewKeyboardKey = NewFirstKey;
SetKeys(NewGamepadKey, NewKeyboardKey);
}
bool UlxPromptWidget::OnTick(float DeltaTime)
{
ElxControllerType Type = UlxInputDeviceTracker::GetLastControllerType();
if (ControllerType != Type)
{
ControllerType = Type;
SynchronizeProperties();
}
return true;
}
TSharedRef<SWidget> UlxPromptWidget::RebuildWidget()
{
if (!IsDesignTime())
{
ControllerType = UlxInputDeviceTracker::GetLastControllerType();
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &UlxPromptWidget::OnTick));
}
MyBrush.SetResourceObject(ButtonAtlas.Get());
SAssignNew(MyImage, SImage).Image(&MyBrush);
SAssignNew(MyGlyph, STextBlock);
SAssignNew(MyScaleBox, SScaleBox)
.Stretch(EStretch::ScaleToFit)
[ MyGlyph.ToSharedRef() ];
MyOverlay = SNew(SOverlay);
MyOverlay->AddSlot().Expose(MyImageSlot)
[ MyImage.ToSharedRef() ];
MyOverlay->AddSlot().HAlign(HAlign_Fill).VAlign(VAlign_Fill).Expose(MyGlyphSlot)
[ MyScaleBox.ToSharedRef() ];
SAssignNew(MyBox, SBox)
[ MyOverlay.ToSharedRef() ];
SynchronizeProperties();
return MyBox.ToSharedRef();
}
void UlxPromptWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
if (!IsDesignTime())
{
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
}
MyBox.Reset();
MyOverlay.Reset();
MyImage.Reset();
MyScaleBox.Reset();
MyGlyph.Reset();
MyImageSlot = nullptr;
MyGlyphSlot = nullptr;
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include "CoreMinimal.h"
#include "Common.h"
#include "Components/Widget.h"
#include "InputCoreTypes.h"
#include "Widgets/SOverlay.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Containers/Ticker.h"
#include "PromptWidget.generated.h"
class UInputAction;
class UInputMappingContext;
UCLASS(BlueprintType, Blueprintable)
class INTEGRATION_API UlxPromptWidget : public UWidget
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category="Prompt")
TObjectPtr<UTexture2D> ButtonAtlas;
UPROPERTY(EditAnywhere, Category="Prompt")
ElxControllerType ControllerType = ElxControllerType::KeyboardMouse;
UPROPERTY(EditAnywhere, Category="Prompt")
FKey GamepadKey = EKeys::Gamepad_FaceButton_Left;
UPROPERTY(EditAnywhere, Category="Prompt")
FKey KeyboardKey = EKeys::Z;
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
FVector2D Size = FVector2D(64, 64);
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
FMargin GlyphMargins = FMargin(0.0f, 0.1f, 0.0f, 0.1f);
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
FLinearColor GlyphColor = FLinearColor::White;
UPROPERTY(EditAnywhere, Setter, Category="Prompt")
bool Depressed = false;
public:
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetKeys(FKey InGamepadKey, FKey InKeyboardKey);
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetKeysFromBindings(const UInputMappingContext* InputMappingContext, const UInputAction* EnhancedInputAction);
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetGlyphMargins(FMargin InMargins);
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetGlyphColor(FLinearColor InColor);
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetSize(FVector2D InSize);
UFUNCTION(BlueprintCallable, Category="Prompt")
void SetDepressed(bool InDepressed);
protected:
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void SynchronizeProperties() override;
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
private:
FSlateBrush MyBrush;
TSharedPtr<SBox> MyBox;
TSharedPtr<SOverlay> MyOverlay;
TSharedPtr<SImage> MyImage;
TSharedPtr<SScaleBox> MyScaleBox;
TSharedPtr<STextBlock> MyGlyph;
SOverlay::FOverlaySlot* MyImageSlot = nullptr;
SOverlay::FOverlaySlot* MyGlyphSlot = nullptr;
FBox2f GetIconUVs(int32 IconIndex);
FMargin GetScaledMargins() const;
int32 ChooseIcon(bool Playstation, FKey Key) const;
void ChooseAppearance(int32 &OutIcon, FString &OutGlyph);
bool OnTick(float DeltaTime);
FTSTicker::FDelegateHandle TickHandle;
};

Some files were not shown because too many files have changed in this diff Show More