-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplugin.js
424 lines (372 loc) · 14.6 KB
/
plugin.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
const fs = require('fs');
const path = require('path');
export default class VoiceMod
{
/*
[Field Variables]
- MOD_NAME (String): The referenceable name of the mod.
- BASE_DIR (String): The top-level directory of the mod. Follows as "assets/mods/<MOD_NAME>/".
- RELATIVE_DIR (String): The directory of the mod relative to "assets/". Follows as "mods/<MOD_NAME>/".
- VOICE_DIR (String): The main directory used for voices. Lowest priority when it comes to overriding.
- PACKS_DIR (String): The secondary directory used for voices. Middle priority when it comes to overriding.
- PACKS (Array): All of the directories (and only directories) found in packs/. Follows as ["demo-pack", ...].
- COMMON_FILE (String): The filename used for the JSON file determining reused dialogue. Must be the same across all voice packs!
- COMMON_DIR (String): The subdirectory used for reused dialogue. Must be the same across all voice packs!
- DATABASE_DIR (String): The subdirectory used for database dialogue. Must be the same across all voice packs!
- MAPS_DIR (String): The subdirectory used for dialogue from individual maps. Must be the same across all voice packs!
- LANG_DIR (String): The subdirectory used for implementing voices on different languages. Must be the same across all voice packs!
- COMMON (Array of Objects): Contains commonly used events for reused dialogue and/or silent dialogue (no sound, no beeps).
- beep (Boolean): Determines whether the text makes sound whenever it progresses. Turned off for lines with voice or lines that are declared silent.
- bestva (Boolean): If bestva is being used, disable all beeps.
*/
// In the future, maybe see if there's a way to do all this asynchronously. It's not good to hold up the system like this.
// Order: Constructor, Preload-Async, Preload, Postload-Async, Postload, Prestart-Async, Prestart, Main-Async, Main.
// Injecting works in Prestart and Main. AJAX Requests work in Postload, Prestart, and Main.
constructor(mod)
{
this.MOD_NAME = mod.name;
this.BASE_DIR = mod.baseDirectory;
this.RELATIVE_DIR = this.BASE_DIR.substring(7); // Gets rid of "assets/".
this.VOICE_DIR = 'voice/';
this.PACKS_DIR = 'packs/';
this.PACKS = this._getPacks();
this.COMMON_FILE = 'common.json';
this.COMMON_DIR = 'common/';
this.DATABASE_DIR = 'database/';
this.MAPS_DIR = 'maps/';
this.LANG_DIR = 'lang/';
this.beep = true;
this.bestva = false;
}
async preload()
{
}
async postload()
{
this.COMMON = await this._getCommons();
}
async prestart()
{
this._inject(this);
}
async poststart()
{
this._injectMain(this);
}
// "mod" will be used to reference the plugin while injecting code since "this" no longer references the plugin while inside those code blocks.
_inject(mod)
{
// It is CRITICAL to have this in prestart in order to detect database entries! Prestart is when database entries load and when you can detect where a message is from.
ig.EVENT_STEP.SHOW_MSG.inject({
voice: null,
beep: true,
init: function()
{
this.parent(...arguments);
this.voice = mod._getVoice(this.message); // This is placed after the parent code because you need to initialize this.message first.
// Retain whether or not this message should beep and reset beep to allow for beeps on non-SHOW_MSG messages.
this.beep = mod.beep;
mod.beep = true;
},
start: function()
{
// This is placed above the voice start to avoid stopping the current line.
new ig.EVENT_STEP.STOP_SOUND({'name':'voice'}).start();
// This is placed before the parent code because you want to figure out if you want to stop the beeps before the message box is instantiated.
if(this.voice)
{
this.voice.start();
this.beep = false;
mod.bestva = false;
}
mod.beep = this.beep; // Set beep if there was a custom setting involved. If there's voice, it'll be set to false anyway.
this.parent(...arguments);
mod.bestva = true;
}
});
ig.EVENT_STEP.SHOW_SIDE_MSG.inject({
voice: null,
beep: true,
init: function()
{
this.parent(...arguments);
this.voice = mod._getVoice(this.message);
this.beep = mod.beep;
mod.beep = true;
},
start: function()
{
new ig.EVENT_STEP.STOP_SOUND({'name':'voice'}).start();
if(this.voice)
{
this.voice.start();
this.beep = false;
mod.bestva = false;
}
mod.beep = this.beep;
this.parent(...arguments);
mod.bestva = true;
}
});
ig.EVENT_STEP.SHOW_OFFSCREEN_MSG.inject({
voice: null,
beep: true,
init: function()
{
this.parent(...arguments);
this.voice = mod._getVoice(this.message);
this.beep = mod.beep;
mod.beep = true;
},
start: function()
{
new ig.EVENT_STEP.STOP_SOUND({'name':'voice'}).start();
if(this.voice)
{
this.voice.start();
this.beep = false;
mod.bestva = false;
}
mod.beep = this.beep;
this.parent(...arguments);
mod.bestva = true;
}
});
ig.EVENT_STEP.SHOW_DREAM_MSG.inject({
voice: null,
init: function()
{
this.parent(...arguments);
this.voice = mod._getVoice(this.text);
this.beep = mod.beep;
mod.beep = true;
},
start: function()
{
new ig.EVENT_STEP.STOP_SOUND({'name':'voice'}).start();
if(this.voice)
{
this.voice.start();
this.beep = false;
mod.bestva = false;
}
mod.beep = this.beep;
this.parent(...arguments);
mod.bestva = true;
}
});
// Redefine PLAY_SOUND to allow for named sounds without requiring that those sounds are looped. This creates the problem that adding named sounds could clog up memory without knowing about it (since you don't have the sound on loop to receive feedback about it). Luckily, this actually solves two problems at once: By clearing previous voice sounds upon starting a new SHOW_MSG event, you avoid the memory clog on the ig.soundManager.namedSounds stack as well as have the ability to stop previous dialogue should the player want to skip over certain lines of dialogue. At worst, you'll be stuck with one unused voice sound which'll clear on the next message or transition to another room.
ig.EVENT_STEP.PLAY_SOUND.inject({
start: function()
{
var a = this.sound.play(this.loop, this.settings);
if(this.name)
ig.soundManager.addNamedSound(this.name, a);
a && this.position && a.setFixPosition(this.position, null);
}
});
// This is to stop all voices when moving from room to room.
// And apparently I can't inject into this unless I put it in prestart.
ig.Game.inject({
teleport: function()
{
new ig.EVENT_STEP.STOP_SOUND({'name':'voice'}).start();
this.parent(...arguments);
}
});
ig.MessageOverlayGui.Entry.inject({
// Injecting into the init function isn't used here because this is called once/twice whenever a person is added, not whenever a message pops up.
addMessage: function()
{
// The advantage of injecting code here rather than sc.MsgBoxGui's init function is that you don't have to worry about messing with ig.dreamFx.isActive(), whether there's a dream going on.
// This is placed before the beepSound is used to let this script determine whether there is a beep or not.
// Additionally, if mod.beep is true but this.beepSound is still null, then reset it. [ERROR: null when testing hideout/entrance, will (probably) conflict with undertale-sfx.] This is just a band aid.
this.beepSound = mod.beep ? (this.beepSound || new ig.Sound('media/sound/hud/dialog-beep-2.ogg', 1, 0.02)) : null;
return this.parent(...arguments); // The original function returns a value, so you have to return the parent call, otherwise there'll be an error.
}
});
// Consider injecting into sc.TextGui. This accounts for both SHOW_MSG and SHOW_SIDE_MSG beeps.
/*sc.TextGui.inject({
update: function()
{
if(mod.beepSound && mod.beepSound.constructor === Array)
this.beepSound = mod.beepSound[mod._getRandom(0, mod.beepSound.length)];
this.parent(...arguments);
}
});*/
// Disable beeps for side messages
sc.SideMessageBoxGui.inject({
setContent: function()
{
this.beepSound = mod.beep ? (this.beepSound || new ig.Sound('media/sound/hud/dialog-beep-2.ogg', 1, 0.02)) : null;
this.parent(...arguments);
}
});
// This is to change the conditions for Best-VA to play.
sc.MessageModel.inject({
showMessage: function(a, b, c)
{
this.boardSide || this.clearBoardMsg();
this._checkActivePerson(a);
this.blocking = true;
this.autoContinue = c;
ig.interact.addEntry(this.screenInteract);
ig.interact.setBlockDelay(0.1);
sc.Model.notifyObserver(this, sc.MESSAGE_EVENT.NEW_MESSAGE,
{
name: a,
text: b
});
if(a = this.currentPeople[a])
{
if(mod.bestva)
sc.voiceActing.play(a.charExpression, b);
this.showSideMessage(a.charExpression, b, true)
}
}
});
}
_injectMain(mod)
{
ig.EVENT_STEP.SHOW_MSG.inject({
init: function()
{
this.parent(...arguments);
if(sc.voiceActing.active)
this.beep = false;
}
});
ig.EVENT_STEP.SHOW_SIDE_MSG.inject({
init: function()
{
this.parent(...arguments);
if(sc.voiceActing.active)
this.beep = false;
}
});
}
/*
[Directories]
Set in order of reverse precedence so that when looping through the list, later settings overwrite previous settings.
assets/mods/voice-mod/voice/...
assets/mods/voice-mod/packs/<pack>/...
It doesn't make sense to have a different object for keeping track of all the packs. Instead, it'll just be an array of pack names, the rest of the path is already taken care of.
["demo-pack", ...]
Then just use assets/mods/voice-mod/packs/demo-pack/...
*/
_getVoice(message)
{
var map = ig.game.mapName; // ie hideout.entrance
var lang = ig.currentLang; // en_US, de_DE, zh_CN, ja_JP, ko_KR, etc.
var langUid = message.data.langUid; // Either undefined or a number
var originFile = message.originFile; // Either null or a string
var src;
if(langUid) // If langUid !== undefined
{
var route = ''; // Just in case neither of the 2 cases below pass.
// The Database Case //
if(originFile === 'data/database.json')
{
route = this.DATABASE_DIR;
map = 'special:database'; // In this case, map will become solely the index of common.json.
}
// The Map Case //
else if(map) // If map !== undefined/null
{
map = map.replace(/\./g,'/'); // ie hideout.entrance --> hideout/entrance, placed here just in case map is undefined (like if you're in the title screen).
route = this.MAPS_DIR + map;
}
// Loops through the main directory and all the packs. Later entries will override previous entries, which is how common.json files will be sorted out.
for(var i = 0; i < this.PACKS.length; i++)
{
var pack = this.PACKS[i] ? this.PACKS_DIR + this.PACKS[i] : this.VOICE_DIR;
// The order here is based on precedence. A language setting overrides the default case, but the default case overrides common events (with the logic that if you're declaring a specific sound file, it should override commonly used ones which might be there by mistake).
// The Specific Language Case //
if(fs.existsSync(path.join(this.BASE_DIR, pack, this.LANG_DIR, lang, route, langUid + '.ogg'))) // ie "assets/mods/<MOD_NAME>/voice/maps/hideout/entrance/#.ogg" exists
src = path.join(this.RELATIVE_DIR, pack, this.LANG_DIR, lang, route, langUid + '.ogg');
// The General Case
else if(fs.existsSync(path.join(this.BASE_DIR, pack, route, langUid + '.ogg'))) // ie "assets/mods/<MOD_NAME>/voice/maps/hideout/entrance/#.ogg" exists
src = path.join(this.RELATIVE_DIR, pack, route, langUid + '.ogg');
// The common.json Case
else if(this.COMMON[i] && this.COMMON[i][map]) // ie If a common.json entry of "hideout/entrance" exists.
{
// Loop through the sounds defined in each map.
for(var sound in this.COMMON[i][map])
{
// Loop through the array of langUids per sound.
for(var j = 0; j < this.COMMON[i][map][sound].length; j++)
{
// If langUids match, fetch the sound.
if(langUid === this.COMMON[i][map][sound][j])
src = this._getCommonSound(sound, pack);
}
}
}
}
}
return src ? new ig.EVENT_STEP.PLAY_SOUND({'sound':src, 'name':'voice'}) : null;
}
_getCommonSound(sound, pack)
{
if(sound.includes(':'))
{
var protocol = sound.substring(0, sound.indexOf(':')); // ie "external" or "special"
var arg = sound.substring(sound.indexOf(':')+1);
// "external" will allow the user to access mods outside the "common" folder.
if(protocol === 'external' && fs.existsSync(path.join('assets/', arg + '.ogg')))
return arg + '.ogg';
else if(protocol === 'special')
{
if(arg === 'silence')
this.beep = false; // Temporarily set beep to false to access this setting outside of the scope of this function.
// Add future cases
}
}
else if(fs.existsSync(path.join(this.BASE_DIR, pack, this.COMMON_DIR, sound + '.ogg')))
return path.join(this.RELATIVE_DIR, pack, this.COMMON_DIR, sound + '.ogg');
}
_getPacks()
{
// The idea behind putting a null as the first item in packs is that when looping through these packs, the first one will route to "voice/".
var packs = [null];
// Reads what's in "assets/mods/<MOD_NAME>/packs/" if "packs/" exists. ie ["demo-pack"]
var list = fs.existsSync(path.join(this.BASE_DIR, this.PACKS_DIR)) ? fs.readdirSync(path.join(this.BASE_DIR, this.PACKS_DIR)) : [];
// For each entry in "packs/", if it's a folder, then add it to packs.
for(var i = 0; i < list.length; i++)
if(fs.statSync(path.join(this.BASE_DIR, this.PACKS_DIR, list[i])).isDirectory())
packs.push(list[i]);
return packs;
}
async _getCommons()
{
var packs = [];
for(var i = 0; i < this.PACKS.length; i++)
{
var pack = this.PACKS[i] ? this.PACKS_DIR + this.PACKS[i] : this.VOICE_DIR;
if(fs.existsSync(path.join(this.BASE_DIR, pack, this.COMMON_FILE)))
{
try
{
packs.push(await this._loadJSON(path.join(this.RELATIVE_DIR, pack, this.COMMON_FILE)));
continue;
}
catch(error)
{
console.error("ERROR: Something's wrong with " + path.join(this.BASE_DIR, pack, this.COMMON_FILE) + "! Use a JSON parser to make sure the file itself is good to go before sending it in a bug report.");
}
}
packs.push(null);
}
return packs;
}
// Usage: await this._loadJSON('mods/voice-mod/package.json');
async _loadJSON(path)
{
return $.ajax({
dataType: 'json',
url: path,
success: (val) => {return val;},
error: (xhr) => {console.error(`Error ${xhr.status}: Could not load "${path}"`);}
});;
}
}