Add controller support and safe main menu hooks
Enable gamepad navigation for the PrismaUI server browser and install MainMenu input hooks safely. Poll XInput each game-thread tick and forward button/stick changes to the browser (ForwardButtonEvent / ForwardThumbstickEvent), allocate an input-enable layer to suppress vanilla kMenu events while the browser is open, and register polling via the F4SE task interface. Replace direct vtable writes that caused a loading-screen crash with REL::Relocation::write_vfunc on the MainMenu BSInputEventUser secondary vtable and install OnButton/OnThumbstick hooks once at plugin Install(). Add MainMenu API to suppress inputEventHandling and call it when showing/hiding the browser. Also update PrismaUI browser JS to support controller focus for direct-connect, filter activation/cycling, improved controller prompts, and add CSS for focused direct-connect fields. Files touched include plugin/src/F4TMenuInput.cpp, plugin/src/F4TMainMenuInject.cpp, plugin/include headers, PrismaUI view JS/components, styles, and documentation.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
connectionStatus: "Online",
|
||||
isRefreshing: false,
|
||||
directConnect: { address: "127.0.0.1", port: "7777" },
|
||||
directConnectFieldIndex: 0,
|
||||
joinOverlay: null
|
||||
};
|
||||
|
||||
@@ -236,7 +237,10 @@
|
||||
state.selectedTabIndex = CO_Tabs.TAB_ORDER.indexOf(tabName);
|
||||
if (state.selectedTabIndex < 0) state.selectedTabIndex = 0;
|
||||
if (tabName === "server-browser") state.focusZone = "server-list";
|
||||
else if (tabName === "direct-connect") state.focusZone = "tabs";
|
||||
else if (tabName === "direct-connect") {
|
||||
state.focusZone = "tabs";
|
||||
state.directConnectFieldIndex = 0;
|
||||
}
|
||||
sendAction("switchTab", { tabName: tabName });
|
||||
render();
|
||||
}
|
||||
@@ -334,11 +338,13 @@
|
||||
}
|
||||
|
||||
function onKeyDown(e) {
|
||||
if (state.joinOverlay && state.joinOverlay.error && e.key === "Escape") {
|
||||
hideJoinOverlay();
|
||||
return;
|
||||
if (state.joinOverlay && state.joinOverlay.error) {
|
||||
if (e.key === "Escape" || e.key === "Enter" || e.key === "b" || e.key === "B") {
|
||||
hideJoinOverlay();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (state.activeTab === "direct-connect") return;
|
||||
if (e.repeat) return;
|
||||
handleControllerButton(mapKeyToButton(e.key), "down");
|
||||
}
|
||||
|
||||
@@ -353,7 +359,9 @@
|
||||
function handleControllerButton(button, phase) {
|
||||
if (!button || phase !== "down") return;
|
||||
if (state.joinOverlay) {
|
||||
if (button === "b") hideJoinOverlay();
|
||||
if (button === "b" || (state.joinOverlay.error && button === "a")) {
|
||||
hideJoinOverlay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -372,6 +380,20 @@
|
||||
}
|
||||
|
||||
if (state.activeTab === "direct-connect") {
|
||||
if (button === "b") {
|
||||
sendAction("closeBrowser", {});
|
||||
return;
|
||||
}
|
||||
if (button === "dpad_up") {
|
||||
state.directConnectFieldIndex = Math.max(0, state.directConnectFieldIndex - 1);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (button === "dpad_down") {
|
||||
state.directConnectFieldIndex = Math.min(1, state.directConnectFieldIndex + 1);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (button === "a") {
|
||||
var vals = CO_DirectConnect.getValues(root);
|
||||
joinDirectLocal(vals.address, vals.port);
|
||||
@@ -396,6 +418,35 @@
|
||||
navigateFocus(button);
|
||||
}
|
||||
|
||||
function cycleFilterValue(filterKey, options) {
|
||||
var current = state.filters[filterKey];
|
||||
var index = options.indexOf(current);
|
||||
if (index < 0) index = 0;
|
||||
var next = options[(index + 1) % options.length];
|
||||
state.filters[filterKey] = next;
|
||||
sendAction("setFilter", { filterName: filterKey, value: next });
|
||||
applyLocalFilters();
|
||||
}
|
||||
|
||||
function activateFocusedFilter() {
|
||||
var maxFilter = CO_Filters.FILTER_ROWS.length;
|
||||
if (state.selectedFilterIndex === maxFilter) {
|
||||
resetLocalFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
var row = CO_Filters.FILTER_ROWS[state.selectedFilterIndex];
|
||||
if (!row) return;
|
||||
|
||||
if (row.type === "select") {
|
||||
cycleFilterValue(row.key, row.options);
|
||||
} else if (row.type === "checkbox") {
|
||||
state.filters[row.key] = !state.filters[row.key];
|
||||
sendAction("setFilter", { filterName: row.key, value: state.filters[row.key] });
|
||||
applyLocalFilters();
|
||||
}
|
||||
}
|
||||
|
||||
function joinSelected() {
|
||||
var sel = getSelectedServer();
|
||||
if (!sel) return;
|
||||
@@ -448,7 +499,7 @@
|
||||
var maxFilter = CO_Filters.FILTER_ROWS.length;
|
||||
if (button === "dpad_up") state.selectedFilterIndex = Math.max(0, state.selectedFilterIndex - 1);
|
||||
if (button === "dpad_down") state.selectedFilterIndex = Math.min(maxFilter, state.selectedFilterIndex + 1);
|
||||
if (button === "a" && state.selectedFilterIndex === maxFilter) resetLocalFilters();
|
||||
if (button === "a") activateFocusedFilter();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
getPrompts: function (state) {
|
||||
if (state.joinOverlay) {
|
||||
if (state.joinOverlay.error) {
|
||||
return [{ key: "B", label: "Dismiss" }];
|
||||
return [
|
||||
{ key: "B", label: "Dismiss" },
|
||||
{ key: "A", label: "Dismiss" }
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -32,12 +35,13 @@
|
||||
];
|
||||
}
|
||||
|
||||
var base = [
|
||||
{ key: "A", label: "Join" },
|
||||
{ key: "X", label: "Refresh" },
|
||||
{ key: "Y", label: "Favorite" },
|
||||
{ key: "B", label: "Back" }
|
||||
];
|
||||
if (state.focusZone === "filters") {
|
||||
var isReset = state.selectedFilterIndex === CO_Filters.FILTER_ROWS.length;
|
||||
return [
|
||||
{ key: "A", label: isReset ? "Reset" : "Change" },
|
||||
{ key: "B", label: "Back" }
|
||||
];
|
||||
}
|
||||
|
||||
if (state.focusZone === "tabs") {
|
||||
return [
|
||||
@@ -47,7 +51,12 @@
|
||||
];
|
||||
}
|
||||
|
||||
return base;
|
||||
return [
|
||||
{ key: "A", label: "Join" },
|
||||
{ key: "X", label: "Refresh" },
|
||||
{ key: "Y", label: "Favorite" },
|
||||
{ key: "B", label: "Back" }
|
||||
];
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
|
||||
var address = state.directConnect.address || "127.0.0.1";
|
||||
var port = state.directConnect.port || "7777";
|
||||
var addressFocused = state.directConnectFieldIndex === 0 ? " focused" : "";
|
||||
var portFocused = state.directConnectFieldIndex === 1 ? " focused" : "";
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="co-direct-row">' +
|
||||
'<div class="co-direct-row' + addressFocused + '">' +
|
||||
'<label for="co-dc-address">Server Address</label>' +
|
||||
'<input class="co-filter-input" id="co-dc-address" type="text" value="' + escapeAttr(address) + '">' +
|
||||
"</div>" +
|
||||
'<div class="co-direct-row">' +
|
||||
'<div class="co-direct-row' + portFocused + '">' +
|
||||
'<label for="co-dc-port">Port</label>' +
|
||||
'<input class="co-filter-input" id="co-dc-port" type="text" value="' + escapeAttr(port) + '">' +
|
||||
"</div>" +
|
||||
|
||||
@@ -630,6 +630,11 @@ body {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.co-direct-row.focused .co-filter-input {
|
||||
outline: 1px solid var(--co-green);
|
||||
background: rgba(var(--co-r), var(--co-g), var(--co-b), 0.08);
|
||||
}
|
||||
|
||||
.co-direct-row label {
|
||||
display: block;
|
||||
font-size: var(--co-font-xs);
|
||||
|
||||
Reference in New Issue
Block a user