Input Binding support

This commit is contained in:
Kae 2023-07-02 17:19:54 +10:00
parent 73841ee041
commit 2386a95342
34 changed files with 1271 additions and 177 deletions

14
.gitignore vendored
View File

@ -25,3 +25,17 @@ install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

View File

@ -0,0 +1,160 @@
{
"scripts" : ["/interface/opensb/bindings/bindings.lua"],
"scriptDelta" : 0,
"scriptWidgetCallbacks" : [
"selectCategory",
"applyBind",
"eraseBind",
"resetBind",
"snare"
],
"gui" : {
"panefeature" : {
"type" : "panefeature",
"positionLocked" : false
},
"background" : {
"type" : "background",
"fileHeader" : "/interface/opensb/bindings/header.png",
"fileBody" : "/interface/opensb/bindings/body.png",
"fileFooter" : "/interface/opensb/bindings/footer.png"
},
"banner" : {
"type" : "canvas",
"rect" : [146, 187, 398, 215]
},
"snare" : {
"type" : "textbox",
"position" : [2147483647, 2147483647],
"regex" : "(){0,0}",
"maxWidth" : 0,
"focus" : false,
"escapeKey": "snare",
"enterKey": "snare",
"callback": "snare"
},
"categories" : {
"type" : "scrollArea",
"rect" : [4, 16, 145, 214],
"children" : {
"list" : {
"type" : "list",
"schema" : {
"selectedBG" : "/interface/opensb/bindings/categoryback.png?multiply=0f0",
"unselectedBG" : "/interface/opensb/bindings/categoryback.png?multiply=222",
"spacing" : [0, 1],
"memberSize" : [130, 16],
"listTemplate" : {
"background" : {
"type" : "image",
"file" : "/interface/opensb/bindings/categoryback.png?multiply=222",
"position" : [0, 0],
"zlevel" : -1
},
"button" : {
"type" : "button",
"callback" : "selectCategory",
"caption" : "Unnamed",
"base" : "/interface/opensb/bindings/category.png?replace;fff=fff0;000=0007",
"hover" : "/interface/opensb/bindings/category.png?replace;fff=fff7;000=3337",
"press" : "/interface/opensb/bindings/category.png?replace;fff=000;000=7777",
"pressedOffset" : [0, 0],
"position" : [0, 0]
}
}
}
}
},
"buttons" : {
"horizontal" : {
"forward" : { "base" : "", "hover" : "", "pressed" : "" },
"backward" : { "base" : "", "hover": "", "pressed" : "" }
},
"vertical" : {
"forward" : {
"base" : "/interface/scrollarea/varrow-forward.png?setcolor=fff",
"hover" : "/interface/scrollarea/varrow-forwardhover.png",
"pressed" : ""
},
"backward" : {
"base" : "/interface/scrollarea/varrow-backward.png?setcolor=fff",
"hover" : "/interface/scrollarea/varrow-backwardhover.png",
"pressed" : ""
}
}
},
"thumbs" : {
"horizontal" : {
"base" : { "begin" : "", "end" : "", "inner" : "" },
"hover" : { "begin" : "", "end" : "", "inner" : "" },
"pressed" : { "begin" : "", "end" : "", "inner" : "" }
},
"vertical" : {
"base" : {
"begin" : "/interface/scrollarea/vthumb-begin.png",
"end" : "/interface/scrollarea/vthumb-end.png",
"inner" : "/interface/scrollarea/vthumb-inner.png"
},
"hover" : {
"begin" : "/interface/scrollarea/vthumb-beginhover.png",
"end" : "/interface/scrollarea/vthumb-endhover.png",
"inner" : "/interface/scrollarea/vthumb-innerhover.png"
},
"pressed" : {
"begin" : "/interface/scrollarea/vthumb-beginhover.png",
"end" : "/interface/scrollarea/vthumb-endhover.png",
"inner" : "/interface/scrollarea/vthumb-innerhover.png"
}
}
}
},
"binds" : {
"type" : "scrollArea",
"rect" : [147, 16, 398, 185],
"children" : {},
"buttons" : {
"horizontal" : {
"forward" : { "base" : "", "hover" : "", "pressed" : "" },
"backward" : { "base" : "", "hover": "", "pressed" : "" }
},
"vertical" : {
"forward" : {
"base" : "/interface/scrollarea/varrow-forward.png?setcolor=fff",
"hover" : "/interface/scrollarea/varrow-forwardhover.png",
"pressed" : ""
},
"backward" : {
"base" : "/interface/scrollarea/varrow-backward.png?setcolor=fff",
"hover" : "/interface/scrollarea/varrow-backwardhover.png",
"pressed" : ""
}
}
},
"thumbs" : {
"horizontal" : {
"base" : { "begin" : "", "end" : "", "inner" : "" },
"hover" : { "begin" : "", "end" : "", "inner" : "" },
"pressed" : { "begin" : "", "end" : "", "inner" : "" }
},
"vertical" : {
"base" : {
"begin" : "/interface/scrollarea/vthumb-begin.png",
"end" : "/interface/scrollarea/vthumb-end.png",
"inner" : "/interface/scrollarea/vthumb-inner.png"
},
"hover" : {
"begin" : "/interface/scrollarea/vthumb-beginhover.png",
"end" : "/interface/scrollarea/vthumb-endhover.png",
"inner" : "/interface/scrollarea/vthumb-innerhover.png"
},
"pressed" : {
"begin" : "/interface/scrollarea/vthumb-beginhover.png",
"end" : "/interface/scrollarea/vthumb-endhover.png",
"inner" : "/interface/scrollarea/vthumb-innerhover.png"
}
}
}
}
}
}

View File

@ -0,0 +1,372 @@
--constants
local PATH = "/interface/opensb/bindings/"
local CATEGORY_LIST_WIDGET = "categories.list"
local BINDS_WIDGET = "binds"
local fmt = string.format
local log = function() end
--SNARE
local snared = false
function snare(key) end
local mods = {}
for i, mod in ipairs{"LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "LGui", "RGui", "AltGr", "Scroll"} do
local data = {name = mod, active = false}
mods[i], mods[mod] = data, data
end
local function getMods(key)
local bindMods = jarray()
for i, mod in ipairs(mods) do
if mod.active and mod.name ~= key then
bindMods[#bindMods + 1] = mod.name
end
end
if bindMods[1] then return bindMods end
end
local function finishBind(type, value)
widget.blur("snare")
snared = false
snareFinished{ type = type, value = value, mods = getMods(value) }
for i, mod in ipairs(mods) do
mod.active = false
end
end
local function scanInputEvents()
local events = input.events()
for i, event in pairs(events) do
local type, data = event.type, event.data
if type == "KeyDown" then
local key = data.key
local mod = mods[key]
if mod then mod.active = true end
elseif type == "KeyUp" then
return finishBind("key", data.key)
elseif type == "MouseButtonDown" then
return finishBind("mouse", data.mouseButton)
end
end
end
function update()
if snared then
scanInputEvents()
if snared and not widget.hasFocus("snare") then
snared = false
snareFinished()
for i, mod in ipairs(mods) do
mod.active = false
end
end
end
end
local function beginSnare()
snared = true
widget.focus("snare")
end
-- BINDING
local function alphabeticalNameSortGreater(a, b) return a.name > b.name end
local function alphabeticalNameSortLesser(a, b) return a.name < b.name end
local sortedCategories = {}
local categories = {}
local widgetsToCategories = {}
local allBinds = {}
local function addCategoryToList(data)
local name = widget.addListItem(CATEGORY_LIST_WIDGET)
widget.setText(fmt("%s.%s.button", CATEGORY_LIST_WIDGET, name), data.name)
log("Added category ^cyan;%s^reset; to list", data.name)
return name
end
local function parseBinds()
for i, path in pairs(root.assetsByExtension("binds")) do
local data = root.assetJson(path)
for categoryId, data in pairs(data) do
if not data.name then data.name = categoryId end
data.categoryId = categoryId
categories[categoryId] = data
end
end
for categoryId, data in pairs(categories) do
sortedCategories[#sortedCategories + 1] = data
end
table.sort(sortedCategories, alphabeticalNameSortLesser)
for i = 1, #sortedCategories do
local data = sortedCategories[i]
data.index = i
local name = addCategoryToList(data)
data.widget = name
widgetsToCategories[name] = data
end
if sortedCategories[1] then
local first = sortedCategories[1].widget
widget.setListSelected(CATEGORY_LIST_WIDGET, first)
selectCategory(first)
end
end
function bindsToString(binds)
local t = {}
for i, bind in pairs(binds) do
local str = ""
if bind.mods then
for i, v in pairs(bind.mods) do
str = str .. v .. " + "
end
end
if bind.type == "key" then
str = str .. bind.value
elseif bind.type == "mouse" then
str = str .. bind.value
end
local _i = (i - 1) * 2
if _i ~= 0 then
t[_i] = ", "
end
t[_i + 1] = str
end
return table.concat(t)
end
function setButtonsEnabled(state)
for i, bind in pairs(allBinds) do
widget.setButtonEnabled(fmt("%s.apply_%s", BINDS_WIDGET, bind.bindId), state)
widget.setButtonEnabled(fmt("%s.erase_%s", BINDS_WIDGET, bind.bindId), state)
widget.setButtonEnabled(fmt("%s.reset_%s", BINDS_WIDGET, bind.bindId), state)
end
for widgetId in pairs(widgetsToCategories) do
widget.setButtonEnabled(fmt("%s.%s.button", CATEGORY_LIST_WIDGET, widgetId), state)
end
widget.setButtonEnabled(CATEGORY_LIST_WIDGET, state)
end
local activeBind
local activeCategory
function snareFinished(newBind)
setButtonsEnabled(true)
if not newBind or not activeBind or not activeCategory then return end
local currentBinds = input.getBinds(activeCategory, activeBind)
local replace = false
for i, v in pairs(currentBinds) do
if sb.printJson(v) == sb.printJson(newBind) then
--equal, replace all binds
currentBinds = {newBind}
replace = true
break
end
end
if replace then
currentBinds = {newBind}
log("Replaced %s with %s", activeBind, sb.printJson(newBind))
else
currentBinds[#currentBinds + 1] = newBind
log("Added %s to %s", sb.printJson(newBind), activeBind)
end
input.setBinds(activeCategory, activeBind, currentBinds)
widget.setText(fmt("%s.apply_%s", BINDS_WIDGET, activeBind), bindsToString(currentBinds))
end
function eraseBind(bind)
bind = bind:sub(7)
local binds = jarray()
binds[1] = nil
input.setBinds(activeCategory, bind, binds)
widget.setText(fmt("%s.apply_%s", BINDS_WIDGET, bind), "")
end
function resetBind(bind)
bind = bind:sub(7)
local defaultBinds = input.getDefaultBinds(activeCategory, bind)
if #defaultBinds == 0 then
defaultBinds = jarray()
defaultBinds[1] = nil
end
input.setBinds(activeCategory, bind, defaultBinds)
widget.setText(fmt("%s.apply_%s", BINDS_WIDGET, bind), bindsToString(defaultBinds))
end
function applyBind(bind)
bind = bind:sub(7)
log("Modifying bind %s", bind)
setButtonsEnabled(false)
activeBind = bind
beginSnare()
end
local function addBindGroup(data, i, added)
local y = (i - 1) * -14
local bg = {
type = "image",
file = PATH .. "groupname.png",
position = {-12, y}
}
local name = "group_" .. i
widget.addChild(BINDS_WIDGET, bg, name)
added[#added + 1] = name
local label = {
type = "label",
value = data.name,
wrapWidth = 120,
fontSize = 8,
hAnchor = "mid",
vAnchor = "mid",
position = {120, 6}
}
widget.addChild(fmt("%s.%s", BINDS_WIDGET, name), label, "text")
end
local function addBindSet(data, i, added)
local y = (i - 1) * -14
local bg = {
type = "image",
file = PATH .. "bindname.png",
position = {-12, y}
}
local name = "label_" .. i
widget.addChild(BINDS_WIDGET, bg, name)
added[#added + 1] = name
local label = {
type = "label",
value = data.name,
wrapWidth = 120,
fontSize = 8,
hAnchor = "mid",
vAnchor = "mid",
position = {62, 6}
}
widget.addChild(fmt("%s.%s", BINDS_WIDGET, name), label, "text")
local button = {
type = "button",
callback = "applyBind",
position = {112, y + 2},
pressedOffset = {0, 0},
base = PATH .. "bind.png",
hover = PATH .. "bind.png?fade=fff;0.025",
pressed = PATH .. "bind.png?fade=fff;0.05?multiply=0f0",
caption = bindsToString(input.getBinds(data.categoryId, data.bindId))
}
name = "apply_" .. data.bindId
added[#added + 1] = name
widget.addChild(BINDS_WIDGET, button, name)
local erase = {
type = "button",
callback = "eraseBind",
position = {209, y + 2},
pressedOffset = {0, -1},
base = PATH .. "garbage.png",
hover = PATH .. "garbage.png?multiply=faa",
pressed = PATH .. "garbage.png?multiply=f00",
}
name = "erase_" .. data.bindId
added[#added + 1] = name
widget.addChild(BINDS_WIDGET, erase, name)
local reset = {
type = "button",
callback = "resetBind",
position = {218, y + 2},
pressedOffset = {0, -1},
base = PATH .. "reset.png",
hover = PATH .. "reset.png?multiply=faa",
pressed = PATH .. "reset.png?multiply=f00",
}
name = "reset_" .. data.bindId
added[#added + 1] = name
widget.addChild(BINDS_WIDGET, reset, name)
end
function selectCategory()
local selected = widget.getListSelected(CATEGORY_LIST_WIDGET)
local category = widgetsToCategories[selected]
local dataFromPrev = widget.getData(BINDS_WIDGET)
if dataFromPrev then
for i, v in pairs(dataFromPrev) do
widget.removeChild(BINDS_WIDGET, v)
end
end
widgetsToBinds = {}
local groups = category.groups or {}
if not groups.unsorted then
groups.unsorted = {name = "Unsorted"}
end
local sortedGroups = {}
for groupId, data in pairs(groups) do
data.name = tostring(data.name or groupId)
data.sortedBinds = {}
sortedGroups[#sortedGroups + 1] = data
end
allBinds = {}
for bindId, data in pairs(category.binds) do
if not data.name then data.name = bindId end
data.bindId = bindId
data.categoryId = category.categoryId
local group = groups[data.group or "unsorted"] or groups.unsorted
group.sortedBinds[#group.sortedBinds + 1] = data
allBinds[#allBinds + 1] = data
end
activeCategory = category.categoryId
table.sort(sortedGroups, alphabeticalNameSortLesser)
for groupId, data in pairs(groups) do
table.sort(data.sortedBinds, alphabeticalNameSortLesser)
end
local bannerBinds = widget.bindCanvas("banner")
bannerBinds:clear()
local bannerName = tostring(category.bannerName or category.name or category.categoryId)
bannerBinds:drawText(bannerName, {position = {127, 13}, horizontalAnchor = "mid", verticalAnchor = "mid"}, 16)
local onlyUnsorted = not sortedGroups[2] and sortedGroups[1] == groups.unsorted
local added = {}
local index = 0
for iA = 1, #sortedGroups do
local group = sortedGroups[iA]
local bindsCount = #group.sortedBinds
if bindsCount > 0 then
if not onlyUnsorted then
index = index + 1
addBindGroup(group, index, added)
end
for iB = 1, bindsCount do
index = index + 1
addBindSet(group.sortedBinds[iB], index, added)
end
end
end
widget.setData(BINDS_WIDGET, added)
end
local function initCallbacks()
widget.registerMemberCallback(CATEGORY_LIST_WIDGET, "selectCategory", selectCategory)
end
function init()
--log = sb.logInfo
widget.clearListItems(CATEGORY_LIST_WIDGET)
initCallbacks()
parseBinds()
script.setUpdateDelta(1)
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

View File

@ -0,0 +1,25 @@
{
"paneLayout" : {
"showKeybindings" : {
"type" : "button",
"position" : [150, 95],
"caption" : "Game Binds",
"base" : "/interface/optionsmenu/controlsbutton.png",
"hover" : "/interface/optionsmenu/controlsbuttonhover.png"
},
"showModBindings" : {
"type" : "button",
"position" : [87, 95],
"caption" : "Mod Binds",
"base" : "/interface/optionsmenu/controlsbutton.png",
"hover" : "/interface/optionsmenu/controlsbuttonhover.png"
},
"showGraphics" : {
"type" : "button",
"position" : [24, 95],
"caption" : "Graphics",
"base" : "/interface/optionsmenu/controlsbutton.png",
"hover" : "/interface/optionsmenu/controlsbuttonhover.png"
}
}
}

View File

@ -207,7 +207,7 @@ void ClientApplication::renderInit(RendererPtr renderer) {
Application::renderInit(renderer);
auto assets = m_root->assets();
auto loadConfig = [&](String const& name) {
auto loadEffectConfig = [&](String const& name) {
String path = strf("/rendering/{}.config", name);
if (assets->assetExists(path)) {
StringMap<String> shaders;
@ -230,8 +230,8 @@ void ClientApplication::renderInit(RendererPtr renderer) {
Logger::warn("No rendering config found for renderer with id '{}'", renderer->rendererId());
};
loadConfig("world");
loadConfig("default");
loadEffectConfig("world");
loadEffectConfig("default");
if (m_root->configuration()->get("limitTextureAtlasSize").optBool().value(false))
renderer->setSizeLimitEnabled(true);
@ -301,17 +301,20 @@ void ClientApplication::processInput(InputEvent const& event) {
if (!m_errorScreen->accepted() && m_errorScreen->handleInputEvent(event))
return;
if (m_state == MainAppState::Splash) {
m_cinematicOverlay->handleInputEvent(event);
bool processed = false;
if (m_state == MainAppState::Splash) {
processed = m_cinematicOverlay->handleInputEvent(event);
} else if (m_state == MainAppState::Title) {
if (!m_cinematicOverlay->handleInputEvent(event))
m_titleScreen->handleInputEvent(event);
if (!(processed = m_cinematicOverlay->handleInputEvent(event)))
processed = m_titleScreen->handleInputEvent(event);
} else if (m_state == MainAppState::SinglePlayer || m_state == MainAppState::MultiPlayer) {
if (!m_cinematicOverlay->handleInputEvent(event))
m_mainInterface->handleInputEvent(event);
if (!(processed = m_cinematicOverlay->handleInputEvent(event)))
processed = m_mainInterface->handleInputEvent(event);
}
m_input->handleInput(event, processed);
}
void ClientApplication::update() {
@ -348,6 +351,7 @@ void ClientApplication::update() {
m_guiContext->cleanup();
m_edgeKeyEvents.clear();
m_input->reset();
}
void ClientApplication::render() {
@ -822,7 +826,9 @@ void ClientApplication::updateRunning() {
m_mainInterface->update();
m_mainMixer->update(m_cinematicOverlay->muteSfx(), m_cinematicOverlay->muteMusic());
appController()->setAcceptingTextInput(m_mainInterface->textInputActive());
bool inputActive = m_mainInterface->textInputActive();
appController()->setAcceptingTextInput(inputActive);
m_input->setTextInputActive(inputActive);
for (auto const& interactAction : m_player->pullInteractActions())
m_mainInterface->handleInteractAction(interactAction);

View File

@ -13,6 +13,8 @@ INCLUDE_DIRECTORIES (
SET (star_frontend_HEADERS
StarActionBar.hpp
StarAiInterface.hpp
StarBaseScriptPane.hpp
StarBindingsMenu.hpp
StarBookmarkInterface.hpp
StarChat.hpp
StarCharCreation.hpp
@ -59,6 +61,8 @@ SET (star_frontend_HEADERS
SET (star_frontend_SOURCES
StarActionBar.cpp
StarAiInterface.cpp
StarBaseScriptPane.cpp
StarBindingsMenu.cpp
StarBookmarkInterface.cpp
StarChat.cpp
StarCharCreation.cpp

View File

@ -0,0 +1,183 @@
#include "StarBaseScriptPane.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
#include "StarGuiReader.hpp"
#include "StarJsonExtra.hpp"
#include "StarConfigLuaBindings.hpp"
#include "StarLuaGameConverters.hpp"
#include "StarWidgetLuaBindings.hpp"
#include "StarCanvasWidget.hpp"
#include "StarItemTooltip.hpp"
#include "StarItemGridWidget.hpp"
#include "StarSimpleTooltip.hpp"
#include "StarImageWidget.hpp"
namespace Star {
BaseScriptPane::BaseScriptPane(Json config) : Pane() {
auto& root = Root::singleton();
auto assets = root.assets();
if (config.type() == Json::Type::Object && config.contains("baseConfig")) {
auto baseConfig = assets->fetchJson(config.getString("baseConfig"));
m_config = jsonMerge(baseConfig, config);
} else {
m_config = assets->fetchJson(config);
}
m_reader.registerCallback("close", [this](Widget*) { dismiss(); });
for (auto const& callbackName : jsonToStringList(m_config.get("scriptWidgetCallbacks", JsonArray{}))) {
m_reader.registerCallback(callbackName, [this, callbackName](Widget* widget) {
m_script.invoke(callbackName, widget->name(), widget->data());
});
}
m_reader.construct(assets->fetchJson(m_config.get("gui")), this);
for (auto pair : m_config.getObject("canvasClickCallbacks", {}))
m_canvasClickCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
for (auto pair : m_config.getObject("canvasKeyCallbacks", {}))
m_canvasKeyCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
m_script.setScripts(jsonToStringList(m_config.get("scripts", JsonArray())));
m_script.setUpdateDelta(m_config.getUInt("scriptDelta", 1));
m_callbacksAdded = false;
}
void BaseScriptPane::show() {
Pane::show();
}
void BaseScriptPane::displayed() {
Pane::displayed();
if (!m_callbacksAdded) {
m_script.addCallbacks("pane", makePaneCallbacks());
m_script.addCallbacks("widget", LuaBindings::makeWidgetCallbacks(this, &m_reader));
m_script.addCallbacks("config", LuaBindings::makeConfigCallbacks( [this](String const& name, Json const& def) {
return m_config.query(name, def);
}));
m_callbacksAdded = true;
}
m_script.init();
m_script.invoke("displayed");
}
void BaseScriptPane::dismissed() {
Pane::dismissed();
m_script.invoke("dismissed");
m_script.uninit();
}
void BaseScriptPane::tick() {
Pane::tick();
for (auto p : m_canvasClickCallbacks) {
for (auto const& clickEvent : p.first->pullClickEvents())
m_script.invoke(p.second, jsonFromVec2I(clickEvent.position), (uint8_t)clickEvent.button, clickEvent.buttonDown);
}
for (auto p : m_canvasKeyCallbacks) {
for (auto const& keyEvent : p.first->pullKeyEvents())
m_script.invoke(p.second, (int)keyEvent.key, keyEvent.keyDown);
}
m_playingSounds.filter([](pair<String, AudioInstancePtr> const& p) {
return p.second->finished() == false;
});
m_script.update(m_script.updateDt());
}
bool BaseScriptPane::sendEvent(InputEvent const& event) {
// Intercept GuiClose before the canvas child so GuiClose always closes
// BaseScriptPanes without having to support it in the script.
if (context()->actions(event).contains(InterfaceAction::GuiClose)) {
dismiss();
return true;
}
return Pane::sendEvent(event);
}
PanePtr BaseScriptPane::createTooltip(Vec2I const& screenPosition) {
auto result = m_script.invoke<Json>("createTooltip", screenPosition);
if (result && !result.value().isNull()) {
if (result->type() == Json::Type::String) {
return SimpleTooltipBuilder::buildTooltip(result->toString());
} else {
PanePtr tooltip = make_shared<Pane>();
m_reader.construct(*result, tooltip.get());
return tooltip;
}
} else {
ItemPtr item;
if (auto child = getChildAt(screenPosition)) {
if (auto itemSlot = as<ItemSlotWidget>(child))
item = itemSlot->item();
if (auto itemGrid = as<ItemGridWidget>(child))
item = itemGrid->itemAt(screenPosition);
}
if (item)
return ItemTooltipBuilder::buildItemTooltip(item);
return {};
}
}
Maybe<String> BaseScriptPane::cursorOverride(Vec2I const& screenPosition) {
auto result = m_script.invoke<Maybe<String>>("cursorOverride", screenPosition);
if (result)
return *result;
else
return {};
}
LuaCallbacks BaseScriptPane::makePaneCallbacks() {
LuaCallbacks callbacks;
callbacks.registerCallback("dismiss", [this]() { dismiss(); });
callbacks.registerCallback("playSound",
[this](String const& audio, Maybe<int> loops, Maybe<float> volume) {
auto assets = Root::singleton().assets();
auto config = Root::singleton().configuration();
auto audioInstance = make_shared<AudioInstance>(*assets->audio(audio));
audioInstance->setVolume(volume.value(1.0));
audioInstance->setLoops(loops.value(0));
auto& guiContext = GuiContext::singleton();
guiContext.playAudio(audioInstance);
m_playingSounds.append({audio, move(audioInstance)});
});
callbacks.registerCallback("stopAllSounds", [this](String const& audio) {
m_playingSounds.filter([audio](pair<String, AudioInstancePtr> const& p) {
if (p.first == audio) {
p.second->stop();
return false;
}
return true;
});
});
callbacks.registerCallback("setTitle", [this](String const& title, String const& subTitle) {
setTitleString(title, subTitle);
});
callbacks.registerCallback("setTitleIcon", [this](String const& image) {
if (auto icon = as<ImageWidget>(titleIcon()))
icon->setImage(image);
});
callbacks.registerCallback("addWidget", [this](Json const& newWidgetConfig, Maybe<String> const& newWidgetName) {
String name = newWidgetName.value(strf("{}", Random::randu64()));
WidgetPtr newWidget = m_reader.makeSingle(name, newWidgetConfig);
this->addChild(name, newWidget);
});
callbacks.registerCallback("removeWidget", [this](String const& widgetName) {
this->removeChild(widgetName);
});
return callbacks;
}
}

View File

@ -0,0 +1,45 @@
#ifndef STAR_BASE_SCRIPT_PANE_HPP
#define STAR_BASE_SCRIPT_PANE_HPP
#include "StarPane.hpp"
#include "StarLuaComponents.hpp"
#include "StarGuiReader.hpp"
namespace Star {
STAR_CLASS(CanvasWidget);
STAR_CLASS(BaseScriptPane);
// A more 'raw' script pane that doesn't depend on a world being present.
// Requires a derived class to provide a Lua root.
class BaseScriptPane : public Pane {
public:
BaseScriptPane(Json config);
virtual void show() override;
void displayed() override;
void dismissed() override;
void tick() override;
bool sendEvent(InputEvent const& event) override;
PanePtr createTooltip(Vec2I const& screenPosition) override;
Maybe<String> cursorOverride(Vec2I const& screenPosition) override;
protected:
virtual LuaCallbacks makePaneCallbacks();
Json m_config;
GuiReader m_reader;
Map<CanvasWidgetPtr, String> m_canvasClickCallbacks;
Map<CanvasWidgetPtr, String> m_canvasKeyCallbacks;
bool m_callbacksAdded;
LuaUpdatableComponent<LuaBaseComponent> m_script;
List<pair<String, AudioInstancePtr>> m_playingSounds;
};
}
#endif

View File

@ -0,0 +1,23 @@
#include "StarBindingsMenu.hpp"
#include "StarInputLuaBindings.hpp"
namespace Star {
BindingsMenu::BindingsMenu(Json const& config) : BaseScriptPane(config) {
m_script.setLuaRoot(make_shared<LuaRoot>());
m_script.addCallbacks("input", LuaBindings::makeInputCallbacks());
}
void BindingsMenu::show() {
BaseScriptPane::show();
}
void BindingsMenu::displayed() {
BaseScriptPane::displayed();
}
void BindingsMenu::dismissed() {
BaseScriptPane::dismissed();
}
}

View File

@ -0,0 +1,24 @@
#ifndef STAR_BINDINGS_MENU_HPP
#define STAR_BINDINGS_MENU_HPP
#include "StarBaseScriptPane.hpp"
namespace Star {
STAR_CLASS(BindingsMenu);
class BindingsMenu : public BaseScriptPane {
public:
BindingsMenu(Json const& config);
virtual void show() override;
void displayed() override;
void dismissed() override;
private:
};
}
#endif

View File

@ -357,7 +357,7 @@ bool MainInterface::handleInputEvent(InputEvent const& event) {
} else if (auto mouseDown = event.ptr<MouseButtonDownEvent>()) {
if (mouseDown->mouseButton == MouseButton::Left || mouseDown->mouseButton == MouseButton::Right
|| mouseDown->mouseButton == MouseButton::Middle)
overlayClick(mouseDown->mousePosition, mouseDown->mouseButton);
return overlayClick(mouseDown->mousePosition, mouseDown->mouseButton);
} else if (auto mouseUp = event.ptr<MouseButtonUpEvent>()) {
if (mouseUp->mouseButton == MouseButton::Left)
@ -1428,7 +1428,7 @@ bool MainInterface::overButton(PolyI buttonPoly, Vec2I const& mousePos) const {
return buttonPoly.contains(mousePos);
}
void MainInterface::overlayClick(Vec2I const& mousePos, MouseButton mouseButton) {
bool MainInterface::overlayClick(Vec2I const& mousePos, MouseButton mouseButton) {
PolyI mainBarPoly = m_config->mainBarPoly;
Vec2I barPos = mainBarPosition();
mainBarPoly.translate(barPos);
@ -1436,17 +1436,17 @@ void MainInterface::overlayClick(Vec2I const& mousePos, MouseButton mouseButton)
if (overButton(m_config->mainBarInventoryButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory);
return;
return true;
}
if (overButton(m_config->mainBarCraftButtonPoly, mousePos)) {
togglePlainCraftingWindow();
return;
return true;
}
if (overButton(m_config->mainBarCodexButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
return;
return true;
}
if (overButton(m_config->mainBarDeployButtonPoly, mousePos)) {
@ -1454,29 +1454,29 @@ void MainInterface::overlayClick(Vec2I const& mousePos, MouseButton mouseButton)
warpToOrbitedWorld(true);
else if (m_client->canBeamUp())
warpToOwnShip();
return;
return true;
}
if (overButton(m_config->mainBarBeamButtonPoly, mousePos)) {
if (m_client->canBeamDown())
warpToOrbitedWorld();
return;
return true;
}
if (overButton(m_config->mainBarQuestLogButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
return;
return true;
}
if (overButton(m_config->mainBarMmUpgradeButtonPoly, mousePos)) {
if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe))
m_paneManager.toggleRegisteredPane(MainInterfacePanes::MmUpgrade);
return;
return true;
}
if (overButton(m_config->mainBarCollectionsButtonPoly, mousePos)) {
m_paneManager.toggleRegisteredPane(MainInterfacePanes::Collections);
return;
return true;
}
if (mouseButton == MouseButton::Left)
@ -1485,6 +1485,8 @@ void MainInterface::overlayClick(Vec2I const& mousePos, MouseButton mouseButton)
m_client->mainPlayer()->beginAltFire();
if (mouseButton == MouseButton::Middle)
m_client->mainPlayer()->beginTrigger();
return false;
}
}

View File

@ -135,7 +135,7 @@ private:
bool overButton(PolyI buttonPoly, Vec2I const& mousePos) const;
void overlayClick(Vec2I const& mousePos, MouseButton mouseButton);
bool overlayClick(Vec2I const& mousePos, MouseButton mouseButton);
GuiContext* m_guiContext;
MainInterfaceConfigConstPtr m_config;

View File

@ -7,6 +7,7 @@
#include "StarLabelWidget.hpp"
#include "StarAssets.hpp"
#include "StarKeybindingsMenu.hpp"
#include "StarBindingsMenu.hpp"
#include "StarGraphicsMenu.hpp"
namespace Star {
@ -48,11 +49,16 @@ OptionsMenu::OptionsMenu(PaneManager* manager)
reader.registerCallback("showKeybindings", [=](Widget*) {
displayControls();
});
reader.registerCallback("showModBindings", [=](Widget*) {
displayModBindings();
});
reader.registerCallback("showGraphics", [=](Widget*) {
displayGraphics();
});
reader.construct(assets->json("/interface/optionsmenu/optionsmenu.config:paneLayout"), this);
Json config = assets->json("/interface/optionsmenu/optionsmenu.config");
reader.construct(config.get("paneLayout"), this);
m_sfxSlider = fetchChild<SliderBarWidget>("sfxSlider");
m_musicSlider = fetchChild<SliderBarWidget>("musicSlider");
@ -68,6 +74,7 @@ OptionsMenu::OptionsMenu(PaneManager* manager)
m_sfxSlider->setRange(m_sfxRange, assets->json("/interface/optionsmenu/optionsmenu.config:sfxDelta").toInt());
m_musicSlider->setRange(m_musicRange, assets->json("/interface/optionsmenu/optionsmenu.config:musicDelta").toInt());
m_modBindingsMenu = make_shared<BindingsMenu>(assets->json(config.getString("bindingsPanePath", "/interface/opensb/bindings/bindings.config")));
m_keybindingsMenu = make_shared<KeybindingsMenu>();
m_graphicsMenu = make_shared<GraphicsMenu>();
@ -162,6 +169,10 @@ void OptionsMenu::displayControls() {
m_paneManager->displayPane(PaneLayer::ModalWindow, m_keybindingsMenu);
}
void OptionsMenu::displayModBindings() {
m_paneManager->displayPane(PaneLayer::ModalWindow, m_modBindingsMenu);
}
void OptionsMenu::displayGraphics() {
m_paneManager->displayPane(PaneLayer::ModalWindow, m_graphicsMenu);
}

View File

@ -12,7 +12,7 @@ STAR_CLASS(ButtonWidget);
STAR_CLASS(LabelWidget);
STAR_CLASS(KeybindingsMenu);
STAR_CLASS(GraphicsMenu);
STAR_CLASS(BindingsMenu);
STAR_CLASS(OptionsMenu);
class OptionsMenu : public Pane {
@ -38,6 +38,7 @@ private:
void syncGuiToConf();
void displayControls();
void displayModBindings();
void displayGraphics();
SliderBarWidgetPtr m_sfxSlider;
@ -58,6 +59,7 @@ private:
JsonObject m_origConfig;
JsonObject m_localChanges;
BindingsMenuPtr m_modBindingsMenu;
KeybindingsMenuPtr m_keybindingsMenu;
GraphicsMenuPtr m_graphicsMenu;
PaneManager* m_paneManager;

View File

@ -20,64 +20,34 @@
namespace Star {
ScriptPane::ScriptPane(UniverseClientPtr client, Json config, EntityId sourceEntityId) {
ScriptPane::ScriptPane(UniverseClientPtr client, Json config, EntityId sourceEntityId) : BaseScriptPane(config) {
auto& root = Root::singleton();
auto assets = root.assets();
m_client = move(client);
if (config.type() == Json::Type::Object && config.contains("baseConfig")) {
auto baseConfig = assets->fetchJson(config.getString("baseConfig"));
m_config = jsonMerge(baseConfig, config);
} else {
m_config = assets->fetchJson(config);
}
m_sourceEntityId = sourceEntityId;
m_reader.registerCallback("close", [this](Widget*) { dismiss(); });
for (auto const& callbackName : jsonToStringList(m_config.get("scriptWidgetCallbacks", JsonArray{}))) {
m_reader.registerCallback(callbackName, [this, callbackName](Widget* widget) {
m_script.invoke(callbackName, widget->name(), widget->data());
});
}
m_reader.construct(assets->fetchJson(m_config.get("gui")), this);
for (auto pair : m_config.getObject("canvasClickCallbacks", {}))
m_canvasClickCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
for (auto pair : m_config.getObject("canvasKeyCallbacks", {}))
m_canvasKeyCallbacks.set(findChild<CanvasWidget>(pair.first), pair.second.toString());
m_script.setScripts(jsonToStringList(m_config.get("scripts", JsonArray())));
m_script.addCallbacks("pane", makePaneCallbacks());
m_script.addCallbacks("widget", LuaBindings::makeWidgetCallbacks(this, &m_reader));
m_script.addCallbacks("config", LuaBindings::makeConfigCallbacks( [this](String const& name, Json const& def) {
return m_config.query(name, def);
}));
m_script.addCallbacks("player", LuaBindings::makePlayerCallbacks(m_client->mainPlayer().get()));
m_script.addCallbacks("status", LuaBindings::makeStatusControllerCallbacks(m_client->mainPlayer()->statusController()));
m_script.addCallbacks("celestial", LuaBindings::makeCelestialCallbacks(m_client.get()));
m_script.setUpdateDelta(m_config.getUInt("scriptDelta", 1));
}
void ScriptPane::displayed() {
Pane::displayed();
auto world = m_client->worldClient();
if (world && world->inWorld())
m_script.init(world.get());
m_script.invoke("displayed");
if (world && world->inWorld()) {
m_script.setLuaRoot(world->luaRoot());
m_script.addCallbacks("world", LuaBindings::makeWorldCallbacks(world.get()));
}
BaseScriptPane::displayed();
}
void ScriptPane::dismissed() {
Pane::dismissed();
m_script.invoke("dismissed");
m_script.uninit();
BaseScriptPane::dismissed();
m_script.removeCallbacks("world");
}
void ScriptPane::tick() {
Pane::tick();
BaseScriptPane::tick();
if (m_sourceEntityId != NullEntityId && !m_client->worldClient()->playerCanReachEntity(m_sourceEntityId))
dismiss();
@ -98,17 +68,6 @@ void ScriptPane::tick() {
m_script.update(m_script.updateDt());
}
bool ScriptPane::sendEvent(InputEvent const& event) {
// Intercept GuiClose before the canvas child so GuiClose always closes
// ScriptPanes without having to support it in the script.
if (context()->actions(event).contains(InterfaceAction::GuiClose)) {
dismiss();
return true;
}
return Pane::sendEvent(event);
}
PanePtr ScriptPane::createTooltip(Vec2I const& screenPosition) {
auto result = m_script.invoke<Json>("createTooltip", screenPosition);
if (result && !result.value().isNull()) {
@ -133,61 +92,9 @@ PanePtr ScriptPane::createTooltip(Vec2I const& screenPosition) {
}
}
Maybe<String> ScriptPane::cursorOverride(Vec2I const& screenPosition) {
auto result = m_script.invoke<Maybe<String>>("cursorOverride", screenPosition);
if (result)
return *result;
else
return {};
}
LuaCallbacks ScriptPane::makePaneCallbacks() {
LuaCallbacks callbacks;
LuaCallbacks callbacks = BaseScriptPane::makePaneCallbacks();
callbacks.registerCallback("sourceEntity", [this]() { return m_sourceEntityId; });
callbacks.registerCallback("dismiss", [this]() { dismiss(); });
callbacks.registerCallback("playSound",
[this](String const& audio, Maybe<int> loops, Maybe<float> volume) {
auto assets = Root::singleton().assets();
auto config = Root::singleton().configuration();
auto audioInstance = make_shared<AudioInstance>(*assets->audio(audio));
audioInstance->setVolume(volume.value(1.0));
audioInstance->setLoops(loops.value(0));
auto& guiContext = GuiContext::singleton();
guiContext.playAudio(audioInstance);
m_playingSounds.append({audio, move(audioInstance)});
});
callbacks.registerCallback("stopAllSounds", [this](String const& audio) {
m_playingSounds.filter([audio](pair<String, AudioInstancePtr> const& p) {
if (p.first == audio) {
p.second->stop();
return false;
}
return true;
});
});
callbacks.registerCallback("setTitle", [this](String const& title, String const& subTitle) {
setTitleString(title, subTitle);
});
callbacks.registerCallback("setTitleIcon", [this](String const& image) {
if (auto icon = as<ImageWidget>(titleIcon()))
icon->setImage(image);
});
callbacks.registerCallback("addWidget", [this](Json const& newWidgetConfig, Maybe<String> const& newWidgetName) {
String name = newWidgetName.value(strf("{}", Random::randu64()));
WidgetPtr newWidget = m_reader.makeSingle(name, newWidgetConfig);
this->addChild(name, newWidget);
});
callbacks.registerCallback("removeWidget", [this](String const& widgetName) {
this->removeChild(widgetName);
});
return callbacks;
}

View File

@ -1,9 +1,7 @@
#ifndef STAR_SCRIPT_PANE_HPP
#define STAR_SCRIPT_PANE_HPP
#include "StarPane.hpp"
#include "StarLuaComponents.hpp"
#include "StarGuiReader.hpp"
#include "StarBaseScriptPane.hpp"
namespace Star {
@ -11,7 +9,7 @@ STAR_CLASS(CanvasWidget);
STAR_CLASS(ScriptPane);
STAR_CLASS(UniverseClient);
class ScriptPane : public Pane {
class ScriptPane : public BaseScriptPane {
public:
ScriptPane(UniverseClientPtr client, Json config, EntityId sourceEntityId = NullEntityId);
@ -20,28 +18,15 @@ public:
void tick() override;
bool sendEvent(InputEvent const& event) override;
PanePtr createTooltip(Vec2I const& screenPosition) override;
Maybe<String> cursorOverride(Vec2I const& screenPosition) override;
bool openWithInventory() const;
private:
LuaCallbacks makePaneCallbacks();
LuaCallbacks makePaneCallbacks() override;
UniverseClientPtr m_client;
EntityId m_sourceEntityId;
Json m_config;
GuiReader m_reader;
Map<CanvasWidgetPtr, String> m_canvasClickCallbacks;
Map<CanvasWidgetPtr, String> m_canvasKeyCallbacks;
LuaWorldComponent<LuaUpdatableComponent<LuaBaseComponent>> m_script;
List<pair<String, AudioInstancePtr>> m_playingSounds;
};
}

View File

@ -234,6 +234,7 @@ SET (star_game_HEADERS
scripting/StarConfigLuaBindings.hpp
scripting/StarEntityLuaBindings.hpp
scripting/StarFireableItemLuaBindings.hpp
scripting/StarInputLuaBindings.hpp
scripting/StarItemLuaBindings.hpp
scripting/StarLuaActorMovementComponent.hpp
scripting/StarLuaAnimationComponent.hpp
@ -472,6 +473,7 @@ SET (star_game_SOURCES
scripting/StarConfigLuaBindings.cpp
scripting/StarEntityLuaBindings.cpp
scripting/StarFireableItemLuaBindings.cpp
scripting/StarInputLuaBindings.cpp
scripting/StarItemLuaBindings.cpp
scripting/StarLuaComponents.cpp
scripting/StarLuaGameConverters.cpp

View File

@ -5,6 +5,21 @@
namespace Star {
const char* InputBindingConfigRoot = "modBindings";
BiMap<Key, KeyMod> const KeysToMods{
{Key::LShift, KeyMod::LShift},
{Key::RShift, KeyMod::RShift},
{Key::LCtrl, KeyMod::LCtrl},
{Key::RCtrl, KeyMod::RCtrl},
{Key::LAlt, KeyMod::LAlt},
{Key::RAlt, KeyMod::RAlt},
{Key::LGui, KeyMod::LGui},
{Key::RGui, KeyMod::RGui},
{Key::AltGr, KeyMod::AltGr},
{Key::ScrollLock, KeyMod::Scroll}
};
const KeyMod KeyModOptional = KeyMod::Num | KeyMod::Caps | KeyMod::Scroll;
inline bool compareKeyModLenient(KeyMod input, KeyMod test) {
@ -33,7 +48,7 @@ Json keyModsToJson(KeyMod mod) {
if ((bool)(mod & KeyMod::AltGr )) array.emplace_back("AltGr" );
if ((bool)(mod & KeyMod::Scroll)) array.emplace_back("Scroll");
return move(array);
return array.empty() ? Json() : move(array);
}
// Optional pointer argument to output calculated priority
@ -115,6 +130,8 @@ Json Input::inputEventToJson(InputEvent const& input) {
Input::Bind Input::bindFromJson(Json const& json) {
Bind bind;
if (json.isNull())
return bind;
String type = json.getString("type");
Json value = json.get("value", {});
@ -143,18 +160,22 @@ Input::Bind Input::bindFromJson(Json const& json) {
Json Input::bindToJson(Bind const& bind) {
if (auto keyBind = bind.ptr<KeyBind>()) {
return JsonObject{
auto obj = JsonObject{
{"type", "key"},
{"value", KeyNames.getRight(keyBind->key)},
{"mods", keyModsToJson(keyBind->mods)}
};
{"value", KeyNames.getRight(keyBind->key)}
}; // don't want empty mods to exist as null entry
if (auto mods = keyModsToJson(keyBind->mods))
obj.emplace("mods", move(mods));
return move(obj);
}
else if (auto mouseBind = bind.ptr<MouseBind>()) {
return JsonObject{
auto obj = JsonObject{
{"type", "mouse"},
{"value", MouseButtonNames.getRight(mouseBind->button)},
{"mods", keyModsToJson(mouseBind->mods)}
{"value", MouseButtonNames.getRight(mouseBind->button)}
};
if (auto mods = keyModsToJson(mouseBind->mods))
obj.emplace("mods", move(mods));
return move(obj);
}
else if (auto controllerBind = bind.ptr<ControllerBind>()) {
return JsonObject{
@ -168,7 +189,7 @@ Json Input::bindToJson(Bind const& bind) {
Input::BindEntry::BindEntry(String entryId, Json const& config, BindCategory const& parentCategory) {
category = &parentCategory;
id = move(entryId);
id = entryId;
name = config.getString("name", id);
for (Json const& jBind : config.getArray("default", {})) {
@ -179,13 +200,55 @@ Input::BindEntry::BindEntry(String entryId, Json const& config, BindCategory con
}
}
void Input::BindEntry::updated() {
auto config = Root::singleton().configuration();
JsonArray array;
for (auto const& bind : customBinds)
array.emplace_back(bindToJson(bind));
if (!config->get(InputBindingConfigRoot).isType(Json::Type::Object))
config->set(InputBindingConfigRoot, JsonObject());
String path = strf("{}.{}", InputBindingConfigRoot, category->id);
if (!config->getPath(path).isType(Json::Type::Object)) {
config->setPath(path, JsonObject{
{ id, move(array) }
});
}
else {
path = strf("{}.{}", path, id);
config->setPath(path, array);
}
Input::singleton().rebuildMappings();
}
Input::BindRef::BindRef(BindEntry& bindEntry, KeyBind& keyBind) {
entry = &bindEntry;
priority = keyBind.priority;
mods = keyBind.mods;
}
Input::BindRef::BindRef(BindEntry& bindEntry, MouseBind& mouseBind) {
entry = &bindEntry;
priority = mouseBind.priority;
mods = mouseBind.mods;
}
Input::BindRef::BindRef(BindEntry& bindEntry) {
entry = &bindEntry;
priority = 0;
mods = KeyMod::NoMod;
}
Input::BindCategory::BindCategory(String categoryId, Json const& categoryConfig) {
id = move(categoryId);
id = categoryId;
config = categoryConfig;
name = config.getString("name", id);
ConfigurationPtr userConfig = Root::singletonPtr()->configuration();
auto userBindings = userConfig->get("modBindings");
auto userBindings = userConfig->get(InputBindingConfigRoot);
for (auto& pair : config.getObject("binds", {})) {
String const& bindId = pair.first;
@ -193,7 +256,7 @@ Input::BindCategory::BindCategory(String categoryId, Json const& categoryConfig)
if (!bindConfig.isType(Json::Type::Object))
continue;
BindEntry& entry = entries.insert(bindId, BindEntry(bindId, bindConfig, *this)).first->second;
BindEntry& entry = entries.try_emplace(bindId, bindId, bindConfig, *this).first->second;
if (userBindings.isType(Json::Type::Object)) {
for (auto& jBind : userBindings.queryArray(strf("{}.{}", id, bindId), {})) {
@ -203,6 +266,9 @@ Input::BindCategory::BindCategory(String categoryId, Json const& categoryConfig)
{ Logger::error("Binds: Error loading user bind in {}.{}: {}", id, bindId, e.what()); }
}
}
if (entry.customBinds.empty())
entry.customBinds = entry.defaultBinds;
}
}
@ -220,6 +286,34 @@ List<Input::BindEntry*> Input::filterBindEntries(List<Input::BindRef> const& bin
return result;
}
Input::BindEntry* Input::bindEntryPtr(String const& categoryId, String const& bindId) {
if (auto category = m_bindCategories.ptr(categoryId)) {
if (auto entry = category->entries.ptr(bindId)) {
return entry;
}
}
return nullptr;
}
Input::BindEntry& Input::bindEntry(String const& categoryId, String const& bindId) {
if (auto ptr = bindEntryPtr(categoryId, bindId))
return *ptr;
else
throw InputException::format("Could not find bind entry {}.{}", categoryId, bindId);
}
Input::InputState* Input::bindStatePtr(String const& categoryId, String const& bindId) {
if (auto ptr = bindEntryPtr(categoryId, bindId))
return m_bindStates.ptr(ptr);
else
return nullptr;
}
Input::InputState* Input::inputStatePtr(InputVariant key) {
return m_inputStates.ptr(key);
}
Input* Input::s_singleton;
Input* Input::singletonPtr() {
@ -239,6 +333,8 @@ Input::Input() {
s_singleton = this;
m_pressedMods = KeyMod::NoMod;
reload();
m_rootReloadListener = make_shared<CallbackListener>([&]() {
@ -256,10 +352,31 @@ List<std::pair<InputEvent, bool>> const& Input::inputEventsThisFrame() const {
return m_inputEvents;
}
void Input::reset() {
m_inputEvents.resize(0); // keeps reserved memory
m_inputStates.clear();
m_bindStates.clear();
{
auto it = m_inputStates.begin();
while (it != m_inputStates.end()) {
if (it->second.held) {
it->second.reset();
++it;
}
else it = m_inputStates.erase(it);
}
}
{
auto it = m_bindStates.begin();
while (it != m_bindStates.end()) {
if (it->second.held) {
it->second.reset();
++it;
}
else it = m_bindStates.erase(it);
}
}
}
void Input::update() {
@ -268,17 +385,83 @@ void Input::update() {
bool Input::handleInput(InputEvent const& input, bool gameProcessed) {
m_inputEvents.emplace_back(input, gameProcessed);
if (auto keyDown = input.ptr<KeyDownEvent>()) {
auto keyToMod = KeysToMods.rightPtr(keyDown->key);
if (keyToMod)
m_pressedMods |= *keyToMod;
if (!gameProcessed && !m_textInputActive) {
m_inputStates[keyDown->key].press();
if (auto binds = m_bindMappings.ptr(keyDown->key)) {
for (auto bind : filterBindEntries(*binds, keyDown->mods))
m_bindStates[bind].press();
}
}
} else if (auto keyUp = input.ptr<KeyUpEvent>()) {
auto keyToMod = KeysToMods.rightPtr(keyUp->key);
if (keyToMod)
m_pressedMods &= ~*keyToMod;
// We need to be able to release input even when gameProcessed is true, but only if it's already down.
if (auto state = m_inputStates.ptr(keyUp->key))
state->release();
if (auto binds = m_bindMappings.ptr(keyUp->key)) {
for (auto& bind : *binds) {
if (auto state = m_bindStates.ptr(bind.entry))
state->release();
}
}
} else if (auto mouseDown = input.ptr<MouseButtonDownEvent>()) {
if (!gameProcessed) {
m_inputStates[mouseDown->mouseButton].press();
if (auto binds = m_bindMappings.ptr(mouseDown->mouseButton)) {
for (auto bind : filterBindEntries(*binds, m_pressedMods))
m_bindStates[bind].press();
}
}
} else if (auto mouseUp = input.ptr<MouseButtonUpEvent>()) {
if (auto state = m_inputStates.ptr(mouseUp->mouseButton))
state->release();
if (auto binds = m_bindMappings.ptr(mouseUp->mouseButton)) {
for (auto& bind : *binds) {
if (auto state = m_bindStates.ptr(bind.entry))
state->release();
}
}
}
return false;
}
void Input::rebuildMappings() {
reset();
m_bindMappings.clear();
for (auto& category : m_bindCategories) {
for (auto& pair : category.second.entries) {
auto& entry = pair.second;
for (auto& bind : entry.customBinds) {
if (auto keyBind = bind.ptr<KeyBind>())
m_bindMappings[keyBind->key].emplace_back(entry, *keyBind);
if (auto mouseBind = bind.ptr<MouseBind>())
m_bindMappings[mouseBind->button].emplace_back(entry, *mouseBind);
if (auto controllerBind = bind.ptr<ControllerBind>())
m_bindMappings[controllerBind->button].emplace_back(entry);
}
}
}
for (auto& pair : m_bindMappings) {
pair.second.sort([](BindRef const& a, BindRef const& b)
{ return a.priority > b.priority; });
}
}
void Input::reload() {
reset();
void Input::reload() {;
m_bindCategories.clear();
auto assets = Root::singleton().assets();
@ -290,7 +473,7 @@ void Input::reload() {
if (!categoryConfig.isType(Json::Type::Object))
continue;
m_bindCategories.insert(categoryId, BindCategory(categoryId, categoryConfig));
m_bindCategories.try_emplace(categoryId, categoryId, categoryConfig);
}
}
@ -303,4 +486,65 @@ void Input::reload() {
rebuildMappings();
}
void Input::setTextInputActive(bool active) {
m_textInputActive = active;
}
Maybe<unsigned> Input::bindDown(String const& categoryId, String const& bindId) {
if (auto state = bindStatePtr(categoryId, bindId))
if (state->presses)
return state->presses;
return {};
}
bool Input::bindHeld(String const& categoryId, String const& bindId) {
if (auto state = bindStatePtr(categoryId, bindId))
return state->held;
else
return false;
}
Maybe<unsigned> Input::bindUp(String const& categoryId, String const& bindId) {
if (auto state = bindStatePtr(categoryId, bindId))
if (state->releases)
return state->releases;
return {};
}
void Input::resetBinds(String const& categoryId, String const& bindId) {
auto& entry = bindEntry(categoryId, bindId);
entry.customBinds = entry.defaultBinds;
entry.updated();
}
Json Input::getDefaultBinds(String const& categoryId, String const& bindId) {
JsonArray array;
for (Bind const& bind : bindEntry(categoryId, bindId).defaultBinds)
array.emplace_back(bindToJson(bind));
return move(array);
}
Json Input::getBinds(String const& categoryId, String const& bindId) {
JsonArray array;
for (Bind const& bind : bindEntry(categoryId, bindId).customBinds)
array.emplace_back(bindToJson(bind));
return move(array);
}
void Input::setBinds(String const& categoryId, String const& bindId, Json const& jBinds) {
auto& entry = bindEntry(categoryId, bindId);
List<Bind> binds;
for (Json const& jBind : jBinds.toArray())
binds.emplace_back(bindFromJson(jBind));
entry.customBinds = move(binds);
entry.updated();
}
}

View File

@ -69,20 +69,17 @@ namespace Star {
List<Bind> customBinds;
BindEntry(String entryId, Json const& config, BindCategory const& parentCategory);
void updated();
};
struct BindRef {
KeyMod mods;
uint8_t priority;
uint8_t priority = 0;
BindEntry* entry = nullptr; // Invalidated on reload, careful!
// Invalidated on reload, careful!
BindEntry* entry;
};
struct BindRefSorter {
inline bool operator() (BindRef const& a, BindRef const& b) {
return a.priority > b.priority;
}
struct BindRef(BindEntry& bindEntry, KeyBind& keyBind);
struct BindRef(BindEntry& bindEntry, MouseBind& mouseBind);
struct BindRef(BindEntry& bindEntry);
};
struct BindCategory {
@ -90,14 +87,17 @@ namespace Star {
String name;
Json config;
StringMap<BindEntry> entries;
StableHashMap<String, BindEntry> entries;
BindCategory(String categoryId, Json const& categoryConfig);
};
struct InputState {
size_t presses = 0;
size_t releases = 0;
unsigned presses = 0;
unsigned releases = 0;
bool pressed = false;
bool held = false;
bool released = false;
// Calls the passed functions for each press and release.
template <typename PressFunction, typename ReleaseFunction>
@ -108,9 +108,13 @@ namespace Star {
}
}
constexpr bool held() {
return presses < releases;
inline void reset() {
presses = releases = 0;
pressed = released = false;
}
inline void press() { pressed = ++presses; held = true; }
inline void release() { released = ++releases; held = false; }
};
// Get pointer to the singleton root instance, if it exists. Otherwise,
@ -141,13 +145,30 @@ namespace Star {
// Loads input categories and their binds from Assets.
void reload();
void setTextInputActive(bool active);
Maybe<unsigned> bindDown(String const& categoryId, String const& bindId);
bool bindHeld(String const& categoryId, String const& bindId);
Maybe<unsigned> bindUp (String const& categoryId, String const& bindId);
void resetBinds(String const& categoryId, String const& bindId);
void setBinds(String const& categoryId, String const& bindId, Json const& binds);
Json getDefaultBinds(String const& categoryId, String const& bindId);
Json getBinds(String const& categoryId, String const& bindId);
private:
List<BindEntry*> filterBindEntries(List<BindRef> const& binds, KeyMod mods) const;
BindEntry* bindEntryPtr(String const& categoryId, String const& bindId);
BindEntry& bindEntry(String const& categoryId, String const& bindId);
InputState* bindStatePtr(String const& categoryId, String const& bindId);
InputState* inputStatePtr(InputVariant key);
static Input* s_singleton;
// Regenerated on reload.
StringMap<BindCategory> m_bindCategories;
StableHashMap<String, BindCategory> m_bindCategories;
// Contains raw pointers to bind entries in categories, so also regenerated on reload.
HashMap<InputVariant, List<BindRef>> m_bindMappings;
@ -160,7 +181,10 @@ namespace Star {
//Input states
HashMap<InputVariant, InputState> m_inputStates;
//Bind states
HashMap<BindEntry*, InputState> m_bindStates;
HashMap<BindEntry const*, InputState> m_bindStates;
KeyMod m_pressedMods;
bool m_textInputActive;
};
}

View File

@ -0,0 +1,36 @@
#include "StarInputLuaBindings.hpp"
#include "StarLuaGameConverters.hpp"
#include "StarInput.hpp"
namespace Star {
LuaCallbacks LuaBindings::makeInputCallbacks() {
LuaCallbacks callbacks;
auto input = Input::singletonPtr();
callbacks.registerCallbackWithSignature<Maybe<unsigned>, String, String>("bindDown", bind(mem_fn(&Input::bindDown), input, _1, _2));
callbacks.registerCallbackWithSignature<bool, String, String>("bindHeld", bind(mem_fn(&Input::bindHeld), input, _1, _2));
callbacks.registerCallbackWithSignature<Maybe<unsigned>, String, String>("bindUp", bind(mem_fn(&Input::bindUp), input, _1, _2));
callbacks.registerCallbackWithSignature<void, String, String>("resetBinds", bind(mem_fn(&Input::resetBinds), input, _1, _2));
callbacks.registerCallbackWithSignature<void, String, String, Json>("setBinds", bind(mem_fn(&Input::setBinds), input, _1, _2, _3));
callbacks.registerCallbackWithSignature<Json, String, String>("getDefaultBinds", bind(mem_fn(&Input::getDefaultBinds), input, _1, _2));
callbacks.registerCallbackWithSignature<Json, String, String>("getBinds", bind(mem_fn(&Input::getBinds), input, _1, _2));
callbacks.registerCallback("events", [input]() -> Json {
JsonArray result;
for (auto& pair : input->inputEventsThisFrame()) {
if (auto jEvent = Input::inputEventToJson(pair.first))
result.emplace_back(jEvent.set("processed", pair.second));
}
return move(result);
});
return callbacks;
}
}

View File

@ -0,0 +1,17 @@
#ifndef STAR_INPUT_LUA_BINDINGS_HPP
#define STAR_INPUT_LUA_BINDINGS_HPP
#include "StarGameTypes.hpp"
#include "StarLua.hpp"
namespace Star {
STAR_CLASS(Input);
namespace LuaBindings {
LuaCallbacks makeInputCallbacks();
}
}
#endif

View File

@ -6,6 +6,7 @@
#include "StarListener.hpp"
#include "StarWorld.hpp"
#include "StarWorldLuaBindings.hpp"
#include "StarInputLuaBindings.hpp"
namespace Star {
@ -282,6 +283,11 @@ void LuaWorldComponent<Base>::init(World* world) {
Base::setLuaRoot(world->luaRoot());
Base::addCallbacks("world", LuaBindings::makeWorldCallbacks(world));
if (world->isClient()) {
Base::addCallbacks("input", LuaBindings::makeInputCallbacks());
}
Base::init();
}
@ -289,6 +295,8 @@ template <typename Base>
void LuaWorldComponent<Base>::uninit() {
Base::uninit();
Base::removeCallbacks("world");
Base::removeCallbacks("input");
}
template <typename Base>