· 6 years ago · Apr 21, 2020, 10:44 AM
1const Promise = require('bluebird');
2const path = require('path');
3const { fs, FlexLayout, log, util } = require('vortex-api');
4const winapi = require('winapi-bindings');
5
6const React = require('react');
7const BS = require('react-bootstrap');
8
9const IniParser = require('vortex-parse-ini');
10
11// Nexus Mods id for the game.
12const GAME_ID = 'starwarsbattlefront22017';
13
14// All SWBF2 mods will be .fbmod files
15const MOD_FILE_EXT = ".fbmod";
16const FROSTY_PATH = 'FrostyModManager';
17const MOD_PATH = path.join(FROSTY_PATH, 'Mods', 'StarWarsBattlefrontII');
18const FROSTY_EXEC = 'FrostyModManager.exe';
19const FROSTY_ID = 'FrostyModManager';
20const FROSTY_CONFIG_FILENAME = 'FrostyModManager starwarsbattlefrontii.ini';
21const I18N_NAMESPACE = 'game-starwarsbattlefront22017';
22let _INI_STRUCT = {};
23
24const tools = [{
25 id: 'FrostyModManagerLaunch',
26 name: 'Launch Modded Game',
27 logo: 'gameart.png',
28 executable: () => FROSTY_EXEC,
29 isPrimary: true,
30 requiredFiles: [
31 FROSTY_EXEC,
32 ],
33 relative: true,
34 exclusive: true,
35 parameters: [
36 '-launch default',
37 ],
38 },
39 {
40 id: FROSTY_ID,
41 name: 'Frosty Mod Manager',
42 logo: 'frosty.png',
43 executable: () => FROSTY_EXEC,
44 requiredFiles: [
45 FROSTY_EXEC,
46 ],
47 relative: true,
48 exclusive: true,
49 }
50];
51
52function getLoadOrderFilePath(context) {
53 const state = context.api.store.getState();
54 const discovery = util.getSafe(state, ['settings', 'gameMode', 'discovered', GAME_ID], undefined);
55
56 return path.join(discovery.path, FROSTY_PATH, FROSTY_CONFIG_FILENAME);
57}
58
59function writeToModSettings() {
60 /*
61 const filePath = getLoadOrderFilePath();
62 const parser = new IniParser.default(new IniParser.WinapiFormat());
63 return fs.removeAsync(filePath).then(() => fs.writeFileAsync(filePath, '', { encoding:'utf8' }))
64 .then(() => parser.read(filePath)).then(ini => {
65 return Promise.each(Object.keys(_INI_STRUCT), (key) => {
66 ini.data[key] = {
67 Enabled: '1',
68 Priority: _INI_STRUCT[key].Priority,
69 VK: _INI_STRUCT[key].VK,
70 }
71 return Promise.resolve();
72 })
73 .then(() => parser.write(filePath, ini));
74 });
75 */
76}
77
78// Attempts to parse and return data found inside
79// the frosty configuration file if found - otherwise this
80// will ensure the file is present.
81function ensureModSettings(context) {
82 const filePath = getLoadOrderFilePath(context);
83 const parser = new IniParser.default(new IniParser.WinapiFormat());
84
85 return fs.statAsync(filePath)
86 .then(() => parser.read(filePath))
87 .catch(err => (err.code === 'ENOENT') ?
88 fs.writeFileAsync(filePath, '', { encoding: 'utf8' }).then(() => parser.read(filePath)) :
89 Promise.reject(err));
90}
91
92function getFrostyConfig(context) {
93 return ensureModSettings(context).then(ini => {
94 // Whole INI config file in array
95 const iniConfig = Object.entries(ini.data);
96 // This is hard-coded and need some help to fix it up
97 const iniProfiles = iniConfig[1][1]; // [Profiles] >> Default
98 // const iniProfileName = Object.getOwnPropertyNames(iniProfiles)[0]; - To obtain profile name
99 const iniLoadOrder = iniProfiles.Default;
100 const regexLoadOrder = /([^:,\|]+):([^|]+)/g;
101 const extractLoadOrder = [...iniLoadOrder.matchAll(regexLoadOrder)];
102 const iniEntries = [];
103
104 for (const modEntry of extractLoadOrder) {
105 console.log('Mod file: ' + modEntry[1]);
106 console.log('Mod state: ' + modEntry[2]);
107 iniEntries.push(modEntry[1]);
108 }
109
110 return iniEntries;
111 })
112 .catch(err => {
113 context.api.showErrorNotification('Failed to lookup mods', err)
114 return Promise.resolve([]);
115 });
116}
117
118async function getAllMods(context) {
119 const frostyMods = await getFrostyConfig(context);
120
121 return Promise.resolve([].concat(frostyMods));
122}
123
124async function setINIStruct(context, loadOrder) {
125 let nextAvailableIdx = Object.keys(loadOrder).length;
126 const getNextIdx = () => {
127 return nextAvailableIdx++;
128 }
129 return getAllMods(context).then(mods => {
130 _INI_STRUCT = {};
131 return Promise.each(mods, mod => {
132 let name;
133 let key;
134 if (typeof(mod) === 'object' && mod !== null) {
135 name = mod.name;
136 key = mod.id;
137 } else {
138 name = mod;
139 key = mod;
140 }
141
142 _INI_STRUCT[name] = {
143 Enabled: '1',
144 Priority: util.getSafe(loadOrder, [key], undefined) !== undefined ?
145 loadOrder[key].pos + 1 : getNextIdx(),
146 VK: key,
147 };
148 });
149 })
150}
151
152async function preSort(context, items, direction) {
153 const frostyMods = await getFrostyConfig(context);
154
155 if ((frostyMods.length === 0)) {
156 return [];
157 }
158
159 const manualEntries = frostyMods
160 .filter(key => (items.find(item => item.id === key) === undefined))
161 .map(key => ({
162 id: key,
163 name: key,
164 imgUrl: `${__dirname}/gameart.png`,
165 }));
166
167 const preSorted = [].concat(...items, ...manualEntries);
168 return (direction === 'descending') ?
169 Promise.resolve(preSorted.reverse()) :
170 Promise.resolve(preSorted);
171}
172
173function findGame() {
174 const instPath = winapi.RegGetValue(
175 'HKEY_LOCAL_MACHINE',
176 'Software\\Wow6432Node\\EA Games\\STAR WARS Battlefront II',
177 'Install Dir');
178 if (!instPath) {
179 throw new Error('empty registry key');
180 }
181 return Promise.resolve(instPath.value);
182}
183
184function prepareForModding(context, discovery) {
185 const notifId = 'missing-frosty';
186 const api = context.api;
187 const missingFrosty = () => api.sendNotification({
188 id: notifId,
189 type: 'info',
190 message: api.translate('Frosty Mod Manager not detected', { ns: I18N_NAMESPACE }),
191 allowSuppress: true,
192 actions: [{
193 title: 'More',
194 action: () => {
195 api.showDialog('info', 'Frosty Mod Manager is missing', {
196 bbcode: api.translate('Vortex is unable to find Frosty Mod Manager. ' +
197 'Please ensure that Frosty Mod Manager is installed in the FrostModManager ' +
198 'folder under the game directory.', { ns: I18N_NAMESPACE }),
199 }, [{
200 label: 'Cancel',
201 action: () => {
202 api.dismissNotification('missing-frosty');
203 }
204 },
205 {
206 label: 'Download Frosty Mod Manager',
207 action: () => util.opn('https://frostytoolsuite.com/downloads.html')
208 .catch(err => null)
209 .then(() => api.dismissNotification('missing-frosty'))
210 },
211 ]);
212 },
213 }, ],
214 });
215
216 const frostyPath = util.getSafe(discovery, ['tools', FROSTY_ID, 'path'], undefined);
217 const findFrosty = () => {
218 return (frostyPath !== undefined) ?
219 fs.statAsync(frostyPath)
220 .catch(() => missingFrosty()) :
221 missingFrosty();
222 };
223 return fs.ensureDirAsync(path.join(discovery.path, MOD_PATH))
224 .tap(() => findFrosty());
225}
226
227function installContent(files) {
228 // The .fbmod file is expected to always be positioned in the mods directory we're going to disregard anything placed outside the root.
229 const modFile = files.find(file => path.extname(file).toLowerCase() === MOD_FILE_EXT);
230 const idx = modFile.indexOf(path.basename(modFile));
231 const rootPath = path.dirname(modFile);
232
233 // Remove directories and anything that isn't in the rootPath.
234 const filtered = files.filter(file =>
235 ((file.indexOf(rootPath) !== -1) &&
236 (!file.endsWith(path.sep))));
237
238 const instructions = filtered.map(file => {
239 return {
240 type: 'copy',
241 source: file,
242 destination: path.join(file.substr(idx)),
243 };
244 });
245
246 return Promise.resolve({ instructions });
247}
248
249function testSupportedContent(files, gameId) {
250 // Make sure we're able to support this mod.
251 let supported = (gameId === GAME_ID) &&
252 (files.find(file => path.extname(file).toLowerCase() === MOD_FILE_EXT) !== undefined);
253
254 // Test for a mod installer.
255 if (supported && files.find(file =>
256 (path.basename(file).toLowerCase() === 'moduleconfig.xml') &&
257 (path.basename(path.dirname(file)).toLowerCase() === 'fomod'))) {
258 supported = false;
259 }
260
261 return Promise.resolve({
262 supported,
263 requiredFiles: [],
264 });
265}
266
267function infoComponent(context, props) {
268 const t = context.api.translate;
269 return React.createElement(BS.Panel, { id: 'loadorderinfo' },
270 React.createElement('h2', {}, t('Managing your load order', { ns: I18N_NAMESPACE })),
271 React.createElement(FlexLayout.Flex, {},
272 React.createElement('p', {},
273 t('You can adjust the load order for Battlefront II by dragging and dropping mods up and down on this page. ' +
274 'This load order is identical to the load order of Frosty Mod Manager. ' +
275 'Any changes made on both Vortex and Frosty will change the actual load order on both sides. ' +
276 'Please consult the individual mod pages for compatiblity issues between mods.', { ns: I18N_NAMESPACE }),
277 )), React.createElement(BS.Button, { onClick: props.refresh }, t('Refresh')));
278}
279
280function main(context) {
281 // Register game extension
282 context.registerGame({
283 id: GAME_ID,
284 name: 'Star Wars: Battlefront II (2017)',
285 mergeMods: true,
286 queryPath: findGame,
287 queryModPath: () => MOD_PATH,
288 logo: 'gameart.png',
289 executable: () => 'starwarsbattlefrontii.exe',
290 setup: (discovery) => prepareForModding(context, discovery),
291 supportedTools: tools,
292 requiredFiles: [
293 'starwarsbattlefrontii.exe'
294 ],
295 });
296
297 context.registerInstaller('starwarsbattlefront22017-mod', 25, testSupportedContent, installContent);
298
299 // Register load order page
300 let previousLO = {};
301 context.registerLoadOrderPage({
302 gameId: GAME_ID,
303 createInfoPanel: (props) => infoComponent(context, props),
304 gameArtURL: `${__dirname}/gameart.png`,
305 preSort: (items, direction) => preSort(context, items, direction),
306 callback: (loadOrder) => {
307 if (loadOrder === previousLO) {
308 return;
309 }
310
311 previousLO = loadOrder;
312 setINIStruct(context, loadOrder)
313 .then(() => writeToModSettings())
314 .catch(err => {
315 context.api.showErrorNotification('Failed to modify load order file', err);
316 return;
317 });
318 },
319 });
320
321 return true;
322}
323
324module.exports = {
325 default: main,
326};