-
Notifications
You must be signed in to change notification settings - Fork 2
/
RunInBackground.js
220 lines (212 loc) · 7.38 KB
/
RunInBackground.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class RunInBackground {
/**
* Current AudioContext that is running audio. null when not running.
* @type {(AudioContext | null)}
*/
ctx = null;
/**
* Whether the class is active, i.e. should be playing audio. Defaults to
* true, because that's what's expected. We persist the value of the setting
* by *modifying the default value* here, via self-modifying code.
* @type {boolean}
*/
active = true;
/**
* Reference to the OptionSwitch function component, hooked from the game.
* @type {(OptionSwitch | null)}
*/
OptionSwitch = null; /* OptionSwitch | null */
/**
* The workerScript object. This is what backs "ns", and having direct
* access gives us incredible power: The ability to make calls without RAM
* usage, call internal functions, and much more. We're only using it to
* make log entries and re-write our code after the script has "died".
* @type {(WorkerScript | null)}
*/
workerScript = null;
/**
* Plays a constant hum (too slow and low to be heard) to prevent backgrounding the tab.
* @param {Event} evt
*/
updateHum(evt) {
// The exact equality means that no event = run always
if (evt?.isTrusted === false) return;
if (this.active === !!this.ctx) return;
if (this.ctx) {
this.ctx.close();
this.ctx = null;
} else {
this.ctx = new AudioContext({ latencyHint: "playback" });
const osc = this.ctx.createOscillator();
// 1Hz - far too low to be audible
osc.frequency.setValueAtTime(1, this.ctx.currentTime);
const gain = this.ctx.createGain();
// This is just above the threshold where playback is considered "silent".
gain.gain.setValueAtTime(0.001, this.ctx.currentTime);
// Have to avoid picking up the RAM cost of singularity.connect
osc["connect"](gain);
gain["connect"](this.ctx.destination);
osc.start();
}
}
/** @returns React.Element to be added to the options menu */
createOption() {
if (!this.OptionSwitch) {
return React.createElement(
"div",
{
style: {
color: "red",
fontSize: "30px",
fontFamily: "sans-serif",
},
},
`Error initializing ${this.workerScript.name}`
);
} else {
return React.createElement(this.OptionSwitch, {
checked: this.active,
onChange: (newValue) => {
if (this.active !== newValue) {
this.active = newValue;
this.writeDefaultValue();
}
this.updateHum();
},
text: "Run in background (unneeded for Steam version)",
tooltip: `If this is set, the game will keep running even when it doesn't have focus.
This uses the audio API, so you may see an audio indicator for the tab but it shouldn't
make noise. If unset and running in a browser, the game will process slowly when the
tab isn't focused. The Steam version always runs, even in the background.`,
});
}
}
writeDefaultValue() {
this.workerScript.print(`Option toggled to ${this.active}`);
// Have to avoid picking this up as an NS function
const server = this.workerScript["getServer"]();
const script = server.scripts.get(this.workerScript.name);
if (!script) {
this.workerScript.print(
`Couldn't find script for ${this.workerScript.name}!`
);
return;
}
// Match the line where we set the default value, and capture the value
// itself, with indices. This is specific enough to not match anything else.
const pat = /^ *active = ([a-z]*);/dm;
// Avoid parsing as ns.exec()
const result = pat["exec"](script.content);
if (!result) {
this.workerScript.print(
`Couldn't match pattern to rewrite the default value in ${this.workerScript.name}!`
);
return;
}
script.content =
script.content.slice(0, result.indices[1][0]) +
String(this.active) +
script.content.slice(result.indices[1][1]);
}
registerHandler() {
// We need to call updateHum from a user action, because Chrome won't give
// us a working AudioContext except in that circumstance. Clicking on the
// settings toggle counts, but we need this listener to kick-start things
// at game start.
// The options mean the listener is in the "capturing" phase (before other
// listeners can cancel event propogation for us), and "passive" because
// we never preventDefault().
globalThis["document"].addEventListener(
"click",
this.updateHum.bind(this),
{ capture: true, passive: true }
);
}
hookReact() {
const orig = React.createElement;
const _this = this;
React.createElement = function (...args) {
const fn = args[0];
const props = args[1];
if (
typeof fn !== "function" ||
props === null ||
typeof props !== "object" ||
props.title !== "System" ||
!String(fn).includes('height:"fit-content"')
) {
return orig.call(this, ...args);
}
if (_this.OptionSwitch === null) {
let i = 2;
for (; i < args.length; ++i) {
const child = args[i];
if (
child !== null &&
typeof child === "object" &&
typeof child.type === "function" &&
String(child.type).includes(".target.checked")
) {
_this.OptionSwitch = child.type;
break;
}
}
if (i >= args.length) {
_this.workerScript.print("Unable to find OptionSwitch!");
}
}
// Add our option to the end of the children
return orig.call(this, ...args, _this.createOption());
};
return orig;
}
/** @param {NS} ns */
hookWorkerScript(ns) {
const orig = Map.prototype.get;
const _this = this;
Map.prototype.get = function (...args) {
// We can safely assume that the first call to this will be the call we
// want, because of synchronous execution.
Map.prototype.get = orig;
_this.workerScript = orig.call(this, ...args);
return _this.workerScript;
};
// This internally has to get the current script, which gets the worker
// script from workerscript map as a first step.
// getSciptLogs() was chosen because it's 0GB and has no effect.
ns.getScriptLogs();
// Just in case something goes wrong.
Map.prototype.get = orig;
if (!_this.workerScript?.print) {
// throw, because we have to abort everything **hard**
let version = "unknown";
try {
version = ns.ui.getGameInfo().version;
} catch (e) {}
throw new Error(
`Couldn't hook WorkerScript! Is this the right version of BitBurner? Was expecting 2.3(ish), got ${version}`
);
}
_this.workerScript.print("Successfully hooked WorkerScript!");
}
/** @param {NS} ns */
constructor(ns) {
ns.printf("Starting...");
this.hookWorkerScript(ns);
// If a cleanup function was registered before, call it.
globalThis.RunInBackgroundCleanupFunc?.();
const origReact = this.hookReact();
globalThis.RunInBackgroundCleanupFunc = () => {
React.createElement = origReact;
};
this.registerHandler();
ns.printf(
`Background audio has started up ${this.active ? "" : "in"}active.`
);
ns.printf("Script will keep functioning in the background.");
}
}
/** @param {NS} ns */
export function main(ns) {
new RunInBackground(ns);
}