Skip to content

Commit a515d49

Browse files
committed
initial commit
0 parents  commit a515d49

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Dmytro Meleshko
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Whitespace-only changes.

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "input-api",
3+
"version": "0.0.0",
4+
"description": "Allows mods to add rebindable key bindings",
5+
"license": "MIT",
6+
"homepage": "https://github.com/dmitmel/input-api",
7+
"module": true,
8+
"postload": "postload.js",
9+
"ccmodDependencies": {
10+
"crosscode": "^1.1.0"
11+
}
12+
}

postload.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// This library is based on the work done by 20kdc in his RaptureUI mod:
2+
// https://github.com/20kdc/decrossfuscator/blob/b5c4250aade5bf8d41b12ec7c346d49ba107b2a9/mods/raptureui/keybinding.js
3+
//
4+
// This mod is needed basically because the developers use a local variable
5+
// (named `KEY_OPTION_MAP` in 0.7.0) which contains an object that maps strings
6+
// (obtained from the keys of the `sc.OPTIONS_DEFINITION`) without the `keys-`
7+
// prefix to the same strings with the said prefix. Why would they do that?
8+
// Hell if I know! Could they implement key rebinding without this conversion
9+
// table? Absolutely. In fact, this is precisely what I've done here. I copied
10+
// the original implementations, but removed references to `KEY_OPTION_MAP` and
11+
// rewrote the functions accordingly.
12+
//
13+
// Unfortunately there is no way to cunningly inject something to leak a
14+
// reference to that variable, so I had to copy copyrighted code, which
15+
// definitely not the best solution, but compared to another variant it gives
16+
// much more flexibility. The other way to accomplish rebinding of modded input
17+
// keys is to define a setter for `OPTIONS_DEFINITION` sometime in postload
18+
// because the module `game.feature.model.options-model`, which populates
19+
// `KEY_OPTION_MAP` and contains definitions of (unsurprisingly)
20+
// `sc.OptionModel`, `sc.KeyBinder` and so on, transitively depends on
21+
// `dom.ready`, therefore by the time of `postload` it wouldn't have been
22+
// executed. This can be done with the following snippet:
23+
//
24+
// ```javascript
25+
// Object.defineProperty(sc, 'OPTIONS_DEFINITION', {
26+
// configurable: true,
27+
// get() {
28+
// return undefined;
29+
// },
30+
// set(value) {
31+
// addCustomOptionsTo(sc.OPTIONS_DEFINITION);
32+
// delete sc.OPTIONS_DEFINITION;
33+
// sc.OPTIONS_DEFINITION = value;
34+
// },
35+
// });
36+
// ```
37+
//
38+
// This is a viable approach, however, 20kdc expressed concerns about this
39+
// killing JIT and as I said above I traded the flexibility for not copying
40+
// original code, so there.
41+
//
42+
// Another point I'd like to bring up is that the original implementation is
43+
// broken and contains bugs. For example, it is possible to unbind primary keys
44+
// (contrary to what UI in `sc.KeyBinderGui` implies): if you want to unbind
45+
// key K1 of binding B1, you simply have to unbind alternative key K2 of the
46+
// binding B2 and bind K1 to that. _I could fix that_. But, this is a library
47+
// mod, and fixing that felt intrusive in the context of a library. So, apart
48+
// from necessary edits, very minor refactors and code deduplications I didn't
49+
// change anything significant.
50+
51+
ig.module('input-api')
52+
.requires('game.feature.model.options-model')
53+
.defines(() => {
54+
// `sc.KEY_BLACK_LIST` contains keys which cannot be bound using graphical
55+
// interface (i.e. `sc.KeyBinderGui`). Really it contains functional keys
56+
// F1-F12 and the control key. The reason for blacklisting functional keys
57+
// is explainable - they are already internally used for the following
58+
// actions:
59+
//
60+
// - [F7 ] opening the typo editor
61+
// - [F8 ] taking screenshots (which can't be saved, trololo)
62+
// - [F10] importing/exporting savestrings
63+
// - [F11] switching to the fullscreen mode
64+
//
65+
// Unfortunately it is not clear to me what did control forget there. The
66+
// jetpack mod makes use of it, so I remove it from the blacklist here.
67+
delete sc.KEY_BLACK_LIST[ig.KEY.CTRL];
68+
69+
sc.KeyBinder.inject({
70+
// the loop which populates `KEY_OPTION_MAP` was moved directly here
71+
initBindings() {
72+
for (let optionId in sc.OPTIONS_DEFINITION) {
73+
let optionDef = sc.OPTIONS_DEFINITION[optionId];
74+
if (optionDef.type !== 'CONTROLS' || !optionId.startsWith('keys-')) {
75+
continue;
76+
}
77+
78+
let action = optionId.slice(5);
79+
let { key1, key2 } = sc.options.values[optionId];
80+
if (key1 != null) {
81+
ig.input.bind(key1, action);
82+
sc.fontsystem.changeKeyCodeIcon(action, key1);
83+
}
84+
if (key2 != null) {
85+
ig.input.bind(key2, action);
86+
}
87+
}
88+
89+
this.updateGamepadIcons();
90+
},
91+
92+
changeBinding(optionId, key, isAlternative, unbind) {
93+
let optionValue = sc.options.values[optionId];
94+
sc.options.hasChanged = true;
95+
96+
// this assignment accessed `KEY_OPTION_MAP` to get the option value
97+
// instead of directly reusing a variable directly
98+
let oldKey = isAlternative ? optionValue.key2 : optionValue.key1;
99+
// this condition seems to handle situations when `oldKey` is
100+
// `undefined` or `null` correctly as well
101+
if (ig.input.bindings[oldKey] != null) ig.input.unbind(oldKey);
102+
103+
if (isAlternative && unbind) {
104+
optionValue.key2 = undefined;
105+
return;
106+
}
107+
108+
let action = optionId.slice(5);
109+
let conflictingAction = ig.input.bindings[key];
110+
111+
ig.input.bind(key, action);
112+
sc.fontsystem.changeKeyCodeIcon(action, key);
113+
114+
if (conflictingAction != null) {
115+
// this assignment used to access `KEY_OPTION_MAP` to get the option
116+
// ID of the conflicting action
117+
let conflictingOption =
118+
sc.options.values[`keys-${conflictingAction}`];
119+
120+
if (conflictingOption.key1 === key) {
121+
conflictingOption.key1 = oldKey;
122+
} else if (conflictingOption.key2 === key) {
123+
conflictingOption.key2 = oldKey;
124+
} else {
125+
// this error message isn't present in the original code, I got this
126+
// idea from 20kdc's implementation
127+
console.error(
128+
'input-api: unable to find the conflicting key binding. report ASAP!',
129+
);
130+
}
131+
132+
ig.input.bind(oldKey, conflictingAction);
133+
sc.fontsystem.changeKeyCodeIcon(conflictingAction, oldKey);
134+
sc.options.dispatchKeySwappedEvent();
135+
}
136+
137+
if (isAlternative) optionValue.key2 = key;
138+
else optionValue.key1 = key;
139+
},
140+
});
141+
});

0 commit comments

Comments
 (0)