From 2386a9534289baf73ce299f33e110f612ff55e38 Mon Sep 17 00:00:00 2001 From: Kae <80987908+Novaenia@users.noreply.github.com> Date: Sun, 2 Jul 2023 17:19:54 +1000 Subject: [PATCH] Input Binding support --- .gitignore | 14 + .../opensb/interface/opensb/bindings/bind.png | Bin 0 -> 418 bytes .../interface/opensb/bindings/bindings.config | 160 ++++++++ .../interface/opensb/bindings/bindings.lua | 372 ++++++++++++++++++ .../interface/opensb/bindings/bindname.png | Bin 0 -> 249 bytes .../opensb/interface/opensb/bindings/body.png | Bin 0 -> 986 bytes .../interface/opensb/bindings/category.png | Bin 0 -> 181 bytes .../opensb/bindings/categoryback.png | Bin 0 -> 318 bytes .../interface/opensb/bindings/footer.png | Bin 0 -> 260 bytes .../interface/opensb/bindings/garbage.png | Bin 0 -> 171 bytes .../interface/opensb/bindings/groupname.png | Bin 0 -> 475 bytes .../interface/opensb/bindings/header.png | Bin 0 -> 601 bytes .../interface/opensb/bindings/reset.png | Bin 0 -> 187 bytes .../interface/optionsmenu/controlsbutton.png | Bin 0 -> 213 bytes .../optionsmenu/controlsbuttonhover.png | Bin 0 -> 213 bytes .../optionsmenu/optionsmenu.config.patch | 25 ++ source/client/StarClientApplication.cpp | 26 +- source/frontend/CMakeLists.txt | 4 + source/frontend/StarBaseScriptPane.cpp | 183 +++++++++ source/frontend/StarBaseScriptPane.hpp | 45 +++ source/frontend/StarBindingsMenu.cpp | 23 ++ source/frontend/StarBindingsMenu.hpp | 24 ++ source/frontend/StarMainInterface.cpp | 22 +- source/frontend/StarMainInterface.hpp | 2 +- source/frontend/StarOptionsMenu.cpp | 13 +- source/frontend/StarOptionsMenu.hpp | 4 +- source/frontend/StarScriptPane.cpp | 113 +----- source/frontend/StarScriptPane.hpp | 21 +- source/game/CMakeLists.txt | 2 + source/game/StarInput.cpp | 278 ++++++++++++- source/game/StarInput.hpp | 56 ++- .../game/scripting/StarInputLuaBindings.cpp | 36 ++ .../game/scripting/StarInputLuaBindings.hpp | 17 + source/game/scripting/StarLuaComponents.hpp | 8 + 34 files changed, 1271 insertions(+), 177 deletions(-) create mode 100644 assets/opensb/interface/opensb/bindings/bind.png create mode 100644 assets/opensb/interface/opensb/bindings/bindings.config create mode 100644 assets/opensb/interface/opensb/bindings/bindings.lua create mode 100644 assets/opensb/interface/opensb/bindings/bindname.png create mode 100644 assets/opensb/interface/opensb/bindings/body.png create mode 100644 assets/opensb/interface/opensb/bindings/category.png create mode 100644 assets/opensb/interface/opensb/bindings/categoryback.png create mode 100644 assets/opensb/interface/opensb/bindings/footer.png create mode 100644 assets/opensb/interface/opensb/bindings/garbage.png create mode 100644 assets/opensb/interface/opensb/bindings/groupname.png create mode 100644 assets/opensb/interface/opensb/bindings/header.png create mode 100644 assets/opensb/interface/opensb/bindings/reset.png create mode 100644 assets/opensb/interface/optionsmenu/controlsbutton.png create mode 100644 assets/opensb/interface/optionsmenu/controlsbuttonhover.png create mode 100644 assets/opensb/interface/optionsmenu/optionsmenu.config.patch create mode 100644 source/frontend/StarBaseScriptPane.cpp create mode 100644 source/frontend/StarBaseScriptPane.hpp create mode 100644 source/frontend/StarBindingsMenu.cpp create mode 100644 source/frontend/StarBindingsMenu.hpp create mode 100644 source/game/scripting/StarInputLuaBindings.cpp create mode 100644 source/game/scripting/StarInputLuaBindings.hpp diff --git a/.gitignore b/.gitignore index 8cd7c60..dc72520 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/assets/opensb/interface/opensb/bindings/bind.png b/assets/opensb/interface/opensb/bindings/bind.png new file mode 100644 index 0000000000000000000000000000000000000000..64efa2022fe3fea69fe1a4d4d20b30818b6919b6 GIT binary patch literal 418 zcmV;T0bTxyP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0XIoRK~zXf?N>{d zgD?!U1IK2`Ex0O)E6FayydEH%m=eVwDF(BtU)L1|b9Za4+1565;zoZ{j7Z;!@>$pZ z{0+W?w|m7?=a);(9e14t9`g>|$(K)!;z~Db(oYR)wd#*HMz_(!0@z1j4}T2BdjLE4 z13AV~2mg*PR88fX8{?LSw4f>N;)ZZo+yPylSw3sUfB7$No8UV%2aY0Y#lt1yCE#k&lrQE#Zq_ox!%0HuAw$=Z#g;L0WlU*bTDYmfJ`9=x zO`2m!BtJQPO#Gm*F?8Pou>A?3wn6ck6Vcvf5p@t3tk81w4m z#Z9cfo=UUxJgKs5rmX=?wl#pswgxcS)&M5k8o*>*{{vVv?SFvT2kOdkj|)3_r~m)} M07*qoM6N<$f=oTF8vp 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 \ No newline at end of file diff --git a/assets/opensb/interface/opensb/bindings/bindname.png b/assets/opensb/interface/opensb/bindings/bindname.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ee548d562ed77f5f9440cfb1f5eaec7a086f8b GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^A3$slHXu1Y@^L7TVk{1FcVbv~PUa<$!;&U>c zv7h@-A}f&3S>O>_%)r2R7=#&*=dVZs3if%rIEG|6zrE$i#jGgca?vj!rs{w76DK2e zkFd!Np1T&#v<~?!KSg<$p4)}lGi_~Fy#BJsR{#9O%4bn5hu-~URp_uUsJ(w?UhzA_ zV|i=e-m8A|H>p97E9QP^!g+P!&k>6cJhE_NKBQrw(8Gj6WiR=)gIzCcZE4VhUlm@F f67Cre({9R7nX7Xq$Z4uR&}9ssu6{1-oD!M<70+7K literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/body.png b/assets/opensb/interface/opensb/bindings/body.png new file mode 100644 index 0000000000000000000000000000000000000000..a63b6fd3954bc10892b41094679e0d49cebfc960 GIT binary patch literal 986 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCpWt8vlE*~1KLS#W#X;^)4C~IxyaaMs(j9#r z85lP9bN@+X1@buyJR*x382Ao@Fyrz36)6l1%=w-!jv*CsZ*LkF%?=Q8xTvvoh3P%{ zoR;1V4&72$4b9G_7%lE8wYGd7vWNd^eKEJ+V~e=ysXNbn{=@ul%l*zoiM-<#*SF<9 zUthys!~gr7|K%WN9;1! zr-4@T+*|wlml4n5w-WDfZF^f|!}Py1b5-6|9JhoJq;iH`&o|s zZG8Uy#JTs3ANgtHE?W3?)J&QrA?gJyJag2@9BlvV$8)&A`q=Jmxz@YwKm3uq`7?9Y awRn-9!q-o(J=y}yj|`r!elF{r5}E+L-3&$m literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/category.png b/assets/opensb/interface/opensb/bindings/category.png new file mode 100644 index 0000000000000000000000000000000000000000..115f33cc6968997b228a15e5506b9cba9b089365 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNb!3HF6tzEtqNHG=%xjQkeJ16rJ$YDu$^mSxl z*x1kgCy^D%=PdAuEM{QfI}E~%$MaXD00q4~T^vI)oZsHuD0sktgTZj}-*Ue>_W6k` zoBs8;Rc=4}l`Apr+^ODw(I4i_Jk8n2ffZIbZ(jSAk@+*f(7eq)Kr0y-JYD@<);T3K F0RSDVIvxN3 literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/categoryback.png b/assets/opensb/interface/opensb/bindings/categoryback.png new file mode 100644 index 0000000000000000000000000000000000000000..3bff20b2ba3ffc2ba68d364746539a0fdde58e53 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNb!3HF6tzEtqNHG=%xjQkeJ16rJ$YDu$^mSxl z*x1kgCy^D%=PdAuEM{QfI}E~%$MaXD00l35x;TbpIKQ2FkngYpk8>mc+y27W?b9^p z1uYF*uy)JsKTEXC_^d0{v)CF;Z+vjg$KlfB_lizE2`o?08-}@d19jQODeB#UP)yMST9XV4mTe{}i(+<^> z=XmVm??$E`yIfQH{G{KUiMh8=eb3kXCnxI2KoHqF3x5+j>Axiq!^(AaQ}|t!56*q5 Swem60dkmhgelF{r5}E*kkASfN literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/footer.png b/assets/opensb/interface/opensb/bindings/footer.png new file mode 100644 index 0000000000000000000000000000000000000000..151f33610a9670f66a12968fc1b2789264a31e99 GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJC^K-BP$x}aOz5`N>#X;^)4C~IxyaaMs(j9#r z85lP9bN@+X1@buyJR*x382Ao@Fyrz36)8Z$nVv3=Ar*0NZyyvqq#)oBn8bgJL$-C7 zlY6Gi>;f}3^?5?y|J-9N%Xn~JZpO39I=$0H`V0ku*>AV9ro>+V%gJ!=YSii6`HTJvus$ v=&CTtxLO?QaR0%1!oZ1%;h{udf6zUz92LuZD|sFO9m(M7>gTe~DWM4fO(t4R literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/garbage.png b/assets/opensb/interface/opensb/bindings/garbage.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3e15ff145db722adc4da0d0b459880ceb37276 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCf`#@Q5sCVBk9p!i>lBSEK+19XwqeLo|YWF9dQiC~z=excT>ejGA=Bf**C8 zOq+~UZWp##_HC%Su&V1+_#2OfVU=D}12z9#@BUPg7P{+GxJd(Z=wIb`o!VjYK(iP; MUHx3vIVCg!0J|qPZ2$lO literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/groupname.png b/assets/opensb/interface/opensb/bindings/groupname.png new file mode 100644 index 0000000000000000000000000000000000000000..c4471141ebc550cf58faa8241c1ad9c5a25da94b GIT binary patch literal 475 zcmV<10VMv3P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!T0;GVScug2F3*seaZW&3tsWyfG4?XO{q22+M9j|EoXPq9$F?UkE+P4*7UThoNS)6 zzIwl;Ri1bCe%U9RyzCC8wqE~y*#q@D1gF}w4_kRJ-@{j~yP^X;48w=Vgpfv%*?m9% zO%p^E`8a326|_Zk(p+ z+hamVLtu8_x~_Y!>-y_CLP#UX?1s?y{pY^#$#aB|Mv&Q!k;6+!o9z3Z{s8CROG8yz Rh`9g&002ovPDHLkV1l3*)LQ@m literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/header.png b/assets/opensb/interface/opensb/bindings/header.png new file mode 100644 index 0000000000000000000000000000000000000000..fa6ec21b79cfaae3499fdf2c46c5c1bb1a3d6beb GIT binary patch literal 601 zcmV-f0;c_mP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0q#jeK~#8N?c0rM z>o61s(6&3gH%A~FC37Vk1u{pwM3IS#o{(%=7KzI{2TFb<>&g`FXNTndt>APzeLkK7 zz}xkD{rh6d=L_fL1X?J|3?D zV3l`w9{{Ws?n2yw0MJE%0MNxpSaiAH?|;JM;c)nx-pJI(sx_(f{cyFLUh$TW ziF20b)s&7;*eFnLx8YZ+MMTp6(s-@OwO$R6XrytCrM;B)AEn1Gtrw(gjIJBB)Nb7$wDLO;o~qQS zTASC1CO4{4u32YP&7ii{mKr1X$N$G22mq_O0|B6$00E$j00E$j00E$j00E$j00E%O n^flhg5T5dh@Q=kwP$wj_hBQVXd800000NkvXXu0mjf_!<%~ literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/opensb/bindings/reset.png b/assets/opensb/interface/opensb/bindings/reset.png new file mode 100644 index 0000000000000000000000000000000000000000..f5168340072e17e1022432894e68b68189e200f5 GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCf`#@Q5sCVBk9p!i>lBSEK+113X0eFPBO8AehupcX7ZP_b1d*s*g28o czySt^$=z1Vb}HyS2b#^`>FVdQ&MBb@0BsjEQUCw| literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/optionsmenu/controlsbutton.png b/assets/opensb/interface/optionsmenu/controlsbutton.png new file mode 100644 index 0000000000000000000000000000000000000000..1a2a6edbd2e20db1767b790a811be50ce143da48 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^c0kO>!3-oFQsZv}DaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheq5(c3u0VS7$@Ys$nX ulzn2nP_`m4g|T3jI@1QH%#Z^I*chB1u{U`-)T9ATVDNPHb6Mw<&;$U%bv#A@ literal 0 HcmV?d00001 diff --git a/assets/opensb/interface/optionsmenu/controlsbuttonhover.png b/assets/opensb/interface/optionsmenu/controlsbuttonhover.png new file mode 100644 index 0000000000000000000000000000000000000000..ba57e02544ca3206c79829f99a78826397e4a3d7 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^c0kO>!3-oFQsZv}DaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheq5(c3u0Zassets(); - auto loadConfig = [&](String const& name) { + auto loadEffectConfig = [&](String const& name) { String path = strf("/rendering/{}.config", name); if (assets->assetExists(path)) { StringMap 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); diff --git a/source/frontend/CMakeLists.txt b/source/frontend/CMakeLists.txt index cd06ce7..50c4229 100644 --- a/source/frontend/CMakeLists.txt +++ b/source/frontend/CMakeLists.txt @@ -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 diff --git a/source/frontend/StarBaseScriptPane.cpp b/source/frontend/StarBaseScriptPane.cpp new file mode 100644 index 0000000..a174622 --- /dev/null +++ b/source/frontend/StarBaseScriptPane.cpp @@ -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(pair.first), pair.second.toString()); + for (auto pair : m_config.getObject("canvasKeyCallbacks", {})) + m_canvasKeyCallbacks.set(findChild(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 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("createTooltip", screenPosition); + if (result && !result.value().isNull()) { + if (result->type() == Json::Type::String) { + return SimpleTooltipBuilder::buildTooltip(result->toString()); + } else { + PanePtr tooltip = make_shared(); + m_reader.construct(*result, tooltip.get()); + return tooltip; + } + } else { + ItemPtr item; + if (auto child = getChildAt(screenPosition)) { + if (auto itemSlot = as(child)) + item = itemSlot->item(); + if (auto itemGrid = as(child)) + item = itemGrid->itemAt(screenPosition); + } + if (item) + return ItemTooltipBuilder::buildItemTooltip(item); + return {}; + } +} + +Maybe BaseScriptPane::cursorOverride(Vec2I const& screenPosition) { + auto result = m_script.invoke>("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 loops, Maybe volume) { + auto assets = Root::singleton().assets(); + auto config = Root::singleton().configuration(); + auto audioInstance = make_shared(*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 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(titleIcon())) + icon->setImage(image); + }); + + callbacks.registerCallback("addWidget", [this](Json const& newWidgetConfig, Maybe 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; +} + +} diff --git a/source/frontend/StarBaseScriptPane.hpp b/source/frontend/StarBaseScriptPane.hpp new file mode 100644 index 0000000..f88cd40 --- /dev/null +++ b/source/frontend/StarBaseScriptPane.hpp @@ -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 cursorOverride(Vec2I const& screenPosition) override; +protected: + virtual LuaCallbacks makePaneCallbacks(); + Json m_config; + + GuiReader m_reader; + + Map m_canvasClickCallbacks; + Map m_canvasKeyCallbacks; + + bool m_callbacksAdded; + LuaUpdatableComponent m_script; + List> m_playingSounds; +}; + +} + +#endif diff --git a/source/frontend/StarBindingsMenu.cpp b/source/frontend/StarBindingsMenu.cpp new file mode 100644 index 0000000..ec56e19 --- /dev/null +++ b/source/frontend/StarBindingsMenu.cpp @@ -0,0 +1,23 @@ +#include "StarBindingsMenu.hpp" +#include "StarInputLuaBindings.hpp" + +namespace Star { + +BindingsMenu::BindingsMenu(Json const& config) : BaseScriptPane(config) { + m_script.setLuaRoot(make_shared()); + m_script.addCallbacks("input", LuaBindings::makeInputCallbacks()); +} + +void BindingsMenu::show() { + BaseScriptPane::show(); +} + +void BindingsMenu::displayed() { + BaseScriptPane::displayed(); +} + +void BindingsMenu::dismissed() { + BaseScriptPane::dismissed(); +} + +} \ No newline at end of file diff --git a/source/frontend/StarBindingsMenu.hpp b/source/frontend/StarBindingsMenu.hpp new file mode 100644 index 0000000..53d08ed --- /dev/null +++ b/source/frontend/StarBindingsMenu.hpp @@ -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 diff --git a/source/frontend/StarMainInterface.cpp b/source/frontend/StarMainInterface.cpp index 2664d2c..86c7bb6 100644 --- a/source/frontend/StarMainInterface.cpp +++ b/source/frontend/StarMainInterface.cpp @@ -357,7 +357,7 @@ bool MainInterface::handleInputEvent(InputEvent const& event) { } else if (auto mouseDown = event.ptr()) { 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()) { 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; } } \ No newline at end of file diff --git a/source/frontend/StarMainInterface.hpp b/source/frontend/StarMainInterface.hpp index aae2890..403b250 100644 --- a/source/frontend/StarMainInterface.hpp +++ b/source/frontend/StarMainInterface.hpp @@ -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; diff --git a/source/frontend/StarOptionsMenu.cpp b/source/frontend/StarOptionsMenu.cpp index 1805a06..cef2e7d 100644 --- a/source/frontend/StarOptionsMenu.cpp +++ b/source/frontend/StarOptionsMenu.cpp @@ -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("sfxSlider"); m_musicSlider = fetchChild("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(assets->json(config.getString("bindingsPanePath", "/interface/opensb/bindings/bindings.config"))); m_keybindingsMenu = make_shared(); m_graphicsMenu = make_shared(); @@ -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); } diff --git a/source/frontend/StarOptionsMenu.hpp b/source/frontend/StarOptionsMenu.hpp index 14158be..bd8c4ba 100644 --- a/source/frontend/StarOptionsMenu.hpp +++ b/source/frontend/StarOptionsMenu.hpp @@ -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; diff --git a/source/frontend/StarScriptPane.cpp b/source/frontend/StarScriptPane.cpp index e2e8a33..38659f1 100644 --- a/source/frontend/StarScriptPane.cpp +++ b/source/frontend/StarScriptPane.cpp @@ -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(pair.first), pair.second.toString()); - for (auto pair : m_config.getObject("canvasKeyCallbacks", {})) - m_canvasKeyCallbacks.set(findChild(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("createTooltip", screenPosition); if (result && !result.value().isNull()) { @@ -133,61 +92,9 @@ PanePtr ScriptPane::createTooltip(Vec2I const& screenPosition) { } } -Maybe ScriptPane::cursorOverride(Vec2I const& screenPosition) { - auto result = m_script.invoke>("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 loops, Maybe volume) { - auto assets = Root::singleton().assets(); - auto config = Root::singleton().configuration(); - auto audioInstance = make_shared(*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 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(titleIcon())) - icon->setImage(image); - }); - - callbacks.registerCallback("addWidget", [this](Json const& newWidgetConfig, Maybe 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; } diff --git a/source/frontend/StarScriptPane.hpp b/source/frontend/StarScriptPane.hpp index 1ba7ea0..d994b68 100644 --- a/source/frontend/StarScriptPane.hpp +++ b/source/frontend/StarScriptPane.hpp @@ -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 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 m_canvasClickCallbacks; - Map m_canvasKeyCallbacks; - - LuaWorldComponent> m_script; - - List> m_playingSounds; }; } diff --git a/source/game/CMakeLists.txt b/source/game/CMakeLists.txt index 97fe8f2..120e9e8 100644 --- a/source/game/CMakeLists.txt +++ b/source/game/CMakeLists.txt @@ -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 diff --git a/source/game/StarInput.cpp b/source/game/StarInput.cpp index a8ac76f..7943ff5 100644 --- a/source/game/StarInput.cpp +++ b/source/game/StarInput.cpp @@ -5,6 +5,21 @@ namespace Star { +const char* InputBindingConfigRoot = "modBindings"; + +BiMap 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()) { - 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()) { - 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()) { 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::filterBindEntries(List 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([&]() { @@ -256,10 +352,31 @@ List> 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()) { + 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()) { + 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()) { + 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()) { + 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()) + m_bindMappings[keyBind->key].emplace_back(entry, *keyBind); + if (auto mouseBind = bind.ptr()) + m_bindMappings[mouseBind->button].emplace_back(entry, *mouseBind); + if (auto controllerBind = bind.ptr()) + 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 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 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 binds; + for (Json const& jBind : jBinds.toArray()) + binds.emplace_back(bindFromJson(jBind)); + + entry.customBinds = move(binds); + entry.updated(); +} + } \ No newline at end of file diff --git a/source/game/StarInput.hpp b/source/game/StarInput.hpp index 6dab146..1c55bc4 100644 --- a/source/game/StarInput.hpp +++ b/source/game/StarInput.hpp @@ -69,20 +69,17 @@ namespace Star { List 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 entries; + StableHashMap 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 @@ -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 bindDown(String const& categoryId, String const& bindId); + bool bindHeld(String const& categoryId, String const& bindId); + Maybe 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 filterBindEntries(List 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 m_bindCategories; + StableHashMap m_bindCategories; // Contains raw pointers to bind entries in categories, so also regenerated on reload. HashMap> m_bindMappings; @@ -160,7 +181,10 @@ namespace Star { //Input states HashMap m_inputStates; //Bind states - HashMap m_bindStates; + HashMap m_bindStates; + + KeyMod m_pressedMods; + bool m_textInputActive; }; } diff --git a/source/game/scripting/StarInputLuaBindings.cpp b/source/game/scripting/StarInputLuaBindings.cpp new file mode 100644 index 0000000..5195e31 --- /dev/null +++ b/source/game/scripting/StarInputLuaBindings.cpp @@ -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, String, String>("bindDown", bind(mem_fn(&Input::bindDown), input, _1, _2)); + callbacks.registerCallbackWithSignature("bindHeld", bind(mem_fn(&Input::bindHeld), input, _1, _2)); + callbacks.registerCallbackWithSignature, String, String>("bindUp", bind(mem_fn(&Input::bindUp), input, _1, _2)); + + callbacks.registerCallbackWithSignature("resetBinds", bind(mem_fn(&Input::resetBinds), input, _1, _2)); + callbacks.registerCallbackWithSignature("setBinds", bind(mem_fn(&Input::setBinds), input, _1, _2, _3)); + callbacks.registerCallbackWithSignature("getDefaultBinds", bind(mem_fn(&Input::getDefaultBinds), input, _1, _2)); + callbacks.registerCallbackWithSignature("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; +} + + +} diff --git a/source/game/scripting/StarInputLuaBindings.hpp b/source/game/scripting/StarInputLuaBindings.hpp new file mode 100644 index 0000000..f0c1f4d --- /dev/null +++ b/source/game/scripting/StarInputLuaBindings.hpp @@ -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 diff --git a/source/game/scripting/StarLuaComponents.hpp b/source/game/scripting/StarLuaComponents.hpp index f370679..ac18106 100644 --- a/source/game/scripting/StarLuaComponents.hpp +++ b/source/game/scripting/StarLuaComponents.hpp @@ -6,6 +6,7 @@ #include "StarListener.hpp" #include "StarWorld.hpp" #include "StarWorldLuaBindings.hpp" +#include "StarInputLuaBindings.hpp" namespace Star { @@ -282,6 +283,11 @@ void LuaWorldComponent::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 void LuaWorldComponent::uninit() { Base::uninit(); Base::removeCallbacks("world"); + + Base::removeCallbacks("input"); } template