synthetic-factorio 0.0.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,49 @@
1
- # Synthetic Factorio
2
-
3
- Synthetic Factorio nodeJS runtime for unit testing mods.
4
-
5
- This is **not** a Factorio emulator, it is not a game that can be played.
6
-
7
- ```typescript
8
- import { ModManager, FactorioEngine } from 'synthetic-factorio';
9
-
10
- const manager = new ModManager();
11
- // install vanilla "mod" data from https://github.com/wube/factorio-data (requires git CLI)
12
- await manager.installVanillaMods();
13
- // install mod by name from mod portal (requires FACTORIO_USERNAME and FACTORIO_TOKEN env variables)
14
- await manager.installPortalMod('SimpleSeablock');
15
- // install a mod in development from local files
16
- manager.installDirectoryMod('my-mod', './my-mod-files', { clearCache: true });
17
-
18
- // initialize the synthetic factorio engine and specify the mods that should be active
19
- // vanilla "mods" need to be specified, including "base", however "core" does not need to be listed
20
- const engine = new FactorioEngine({
21
- mods: ['base', 'space-age', 'quality', 'recycler', 'elevated-rails', 'my-mod']
22
- });
23
- // run all mods through their settings phase
24
- engine.runSettingsPhase();
25
- // customize settings for your specific test run
26
- engine.setSettings({ startup: { 'my-mod-setting': true } });
27
- // run all mods through their data phase
28
- engine.runDataPhase();
29
-
30
- // read the data.raw collection and run test assertions
31
- console.log(engine.getRawData()['item']['stone-furnace']);
32
-
33
- // run control phase - NOTE: control phase support is currently very limited
34
- engine.runControlPhase(['my-mod']);
35
-
36
- // run any registered on_init callbacks
37
- engine.triggerInit();
38
-
39
- // trigger an event with the payload of your choice
40
- // Partial on the generic and casts on the sub-properties are recommended to avoid the need to setup the entire event payload
41
- engine.triggerEvent<Partial<runtime.on_chunk_generated>>(defines.events.on_chunk_generated, {
42
- position: { x: 0, y: 0 },
43
- surface: { name: 'nauvis' } as runtime.LuaSurface,
44
- });
45
- ```
1
+ # Synthetic Factorio
2
+
3
+ <!--Hidden version bump 4-->
4
+
5
+ Synthetic Factorio nodeJS runtime for unit testing mods.
6
+
7
+ This is **not** a Factorio emulator, it is not a game that can be played.
8
+
9
+ ```typescript
10
+ // defines are imported from synthetic-factorio not factorio-types.
11
+ // In factorio-types they're an ambient declaration, in synthetic-factorio they're a usable object.
12
+ import { ModManager, FactorioEngine, defines } from 'synthetic-factorio';
13
+
14
+ const manager = new ModManager();
15
+ // install vanilla "mod" data from https://github.com/wube/factorio-data (requires git CLI)
16
+ await manager.installVanillaMods();
17
+ // install mod by name from mod portal (requires FACTORIO_USERNAME and FACTORIO_TOKEN env variables)
18
+ await manager.installPortalMod('SimpleSeablock');
19
+ // install a mod in development from local files
20
+ manager.installDirectoryMod('my-mod', './my-mod-files', { clearCache: true });
21
+
22
+ // initialize the synthetic factorio engine and specify the mods that should be active
23
+ // vanilla "mods" need to be specified, including "base", however "core" does not need to be listed
24
+ const engine = new FactorioEngine({
25
+ mods: ['base', 'space-age', 'quality', 'recycler', 'elevated-rails', 'my-mod']
26
+ });
27
+ // run all mods through their settings phase
28
+ engine.runSettingsPhase();
29
+ // customize settings for your specific test run
30
+ engine.setSettings({ startup: { 'my-mod-setting': true } });
31
+ // run all mods through their data phase
32
+ engine.runDataPhase();
33
+
34
+ // read the data.raw collection and run test assertions
35
+ console.log(engine.getRawData()['item']['stone-furnace']);
36
+
37
+ // run control phase - NOTE: control phase support is currently very limited
38
+ engine.runControlPhase(['my-mod']);
39
+
40
+ // run any registered on_init callbacks
41
+ engine.triggerInit();
42
+
43
+ // trigger an event with the payload of your choice
44
+ // Partial on the generic and casts on the sub-properties are recommended to avoid the need to setup the entire event payload
45
+ engine.triggerEvent<Partial<runtime.on_chunk_generated>>(defines.events.on_chunk_generated, {
46
+ position: { x: 0, y: 0 },
47
+ surface: { name: 'nauvis' } as runtime.LuaSurface,
48
+ });
49
+ ```
@@ -7,6 +7,11 @@ export interface ModManagerOptions {
7
7
  * Working directory for files like mod downloads
8
8
  */
9
9
  workingDir?: string;
10
+ /**
11
+ * Function to accept diagnostic logging, if not supplied then no diagnostics will be logged. Can be verbose, primarily intended for troubleshooting
12
+ * @param message Message to be logged
13
+ */
14
+ logger?: (message: string) => void;
10
15
  }
11
16
  export interface ModInstallOptions {
12
17
  /**
@@ -44,6 +49,7 @@ export interface ModPortalDownloadOptions extends ModDownloadOptions {
44
49
  export declare class ModManager {
45
50
  readonly modDir: string;
46
51
  readonly workingDir: string;
52
+ readonly log: (message: string) => void;
47
53
  constructor(options?: ModManagerOptions);
48
54
  /**
49
55
  * Download all vanilla "mod" data from https://github.com/wube/factorio-data
@@ -16,9 +16,11 @@ const vanillaMods = ['core', 'base', 'quality', 'recycler', 'elevated-rails', 's
16
16
  export class ModManager {
17
17
  modDir;
18
18
  workingDir;
19
+ log;
19
20
  constructor(options) {
20
21
  this.modDir = options?.modDir || modDir();
21
22
  this.workingDir = options?.workingDir || workingDir();
23
+ this.log = options?.logger || (() => { });
22
24
  }
23
25
  /**
24
26
  * Download all vanilla "mod" data from https://github.com/wube/factorio-data
@@ -28,13 +30,17 @@ export class ModManager {
28
30
  async installVanillaMods(options) {
29
31
  const run = promisify(exec);
30
32
  const gitDir = path.join(this.workingDir, 'factorio-data');
33
+ this.log(`Installing vanilla mods at ${gitDir}`);
31
34
  if (existsSync(gitDir) && options?.clearDownload) {
35
+ this.log(`gitDir ${gitDir} exists, deleting`);
32
36
  rmSync(gitDir, { recursive: true });
33
37
  }
34
38
  if (existsSync(gitDir) && options?.checkLatest) {
39
+ this.log(`Vanilla mod repo exists at ${gitDir}, pulling latest`);
35
40
  await run('git pull', { cwd: gitDir });
36
41
  }
37
42
  if (!existsSync(gitDir)) {
43
+ this.log(`Vanilla mod repo does not exist at ${gitDir}, cloning`);
38
44
  mkdirSync(this.workingDir, { recursive: true });
39
45
  await run('git clone --depth=1 https://github.com/wube/factorio-data.git', { cwd: this.workingDir });
40
46
  }
@@ -50,12 +56,18 @@ export class ModManager {
50
56
  */
51
57
  installDirectoryMod(modName, sourcePath, options) {
52
58
  const targetPath = this.getModPath(modName);
59
+ this.log(`Installing mod ${modName} to ${targetPath} from ${sourcePath}`);
53
60
  if (existsSync(targetPath) && options?.clearCache) {
61
+ this.log(`Removing existing mod ${modName} at ${targetPath}`);
54
62
  rmSync(targetPath, { recursive: true });
55
63
  }
56
64
  if (!existsSync(targetPath)) {
65
+ this.log(`Copying mod ${modName} from ${sourcePath} to ${targetPath}`);
57
66
  cpSync(sourcePath, targetPath, { recursive: true });
58
67
  }
68
+ else {
69
+ this.log(`Mod ${modName} already exists at ${targetPath}`);
70
+ }
59
71
  }
60
72
  /**
61
73
  * Downloads and installs a mod from Factorio's mod portal.
@@ -64,24 +76,29 @@ export class ModManager {
64
76
  * @param options Mod Installation Optiions
65
77
  */
66
78
  async installPortalMod(modName, options) {
79
+ this.log(`Installing ${modName} from portal`);
67
80
  let modInfo = undefined;
68
81
  let targetVersion = undefined;
69
82
  const downloadDir = path.join(this.workingDir, 'downloads');
70
83
  mkdirSync(downloadDir, { recursive: true });
71
84
  if (options?.version) {
72
85
  targetVersion = options?.version;
86
+ this.log(`Mod ${modName} installed at specific version ${targetVersion}`);
73
87
  }
74
88
  else if (options?.checkLatest) {
75
89
  modInfo = await this.getModInfo(modName);
76
90
  targetVersion = modInfo.releases.slice(-1)[0].version;
91
+ this.log(`Mod ${modName} latest version is ${targetVersion}`);
77
92
  }
78
93
  let zipPath = undefined;
79
94
  if (targetVersion) {
80
95
  zipPath = path.join(downloadDir, `${modName}_${targetVersion}.zip`);
96
+ this.log(`Installing mod ${modName} at target version ${targetVersion} to ${zipPath}`);
81
97
  }
82
98
  else {
83
99
  const modFiles = await glob(`${modName}_*.zip`, { cwd: downloadDir });
84
100
  if (modFiles.length) {
101
+ this.log(`Found ${modFiles.length} existing versions of mod ${modName}`);
85
102
  let latest = '0.0.0';
86
103
  for (const file of modFiles) {
87
104
  const version = file.replace(/\.zip$/, '').split('_').slice(-1)[0];
@@ -89,10 +106,16 @@ export class ModManager {
89
106
  latest = version;
90
107
  }
91
108
  }
109
+ this.log(`Latest downloaded version of ${modName} is ${latest}`);
92
110
  zipPath = path.join(downloadDir, `${modName}_${latest}.zip`);
111
+ this.log(`Using zip path ${zipPath} for ${modName}`);
112
+ }
113
+ else {
114
+ this.log(`Did not find previously-installed version of ${modName}`);
93
115
  }
94
116
  }
95
117
  if (zipPath && options?.clearDownload) {
118
+ this.log(`Deleting existing zip at ${zipPath} for ${modName}`);
96
119
  rmSync(zipPath);
97
120
  }
98
121
  if (!zipPath) {
@@ -100,7 +123,9 @@ export class ModManager {
100
123
  modInfo = await this.getModInfo(modName);
101
124
  }
102
125
  targetVersion = modInfo.releases.slice(-1)[0].version;
126
+ this.log(`Found ${targetVersion} version of ${modName} from mod info`);
103
127
  zipPath = path.join(downloadDir, `${modName}_${targetVersion}.zip`);
128
+ this.log(`Using zip path ${zipPath} for ${modName}`);
104
129
  }
105
130
  if (!existsSync(zipPath)) {
106
131
  if (!modInfo) {
@@ -111,6 +136,7 @@ export class ModManager {
111
136
  let url = modInfo.releases.find(r => r.version === targetVersion)?.download_url;
112
137
  if (url) {
113
138
  modDownloadUrl = url;
139
+ this.log(`Download url for ${modName} at target version ${targetVersion} is ${modDownloadUrl}`);
114
140
  }
115
141
  else {
116
142
  throw new Error(`Cannot find version ${targetVersion} of mod ${modName}`);
@@ -118,6 +144,7 @@ export class ModManager {
118
144
  }
119
145
  else {
120
146
  modDownloadUrl = modInfo.releases.slice(-1)[0].download_url;
147
+ this.log(`Download url for ${modName} is ${modDownloadUrl}`);
121
148
  }
122
149
  if (!process.env.FACTORIO_USERNAME) {
123
150
  throw new Error('Cannot download mod, env variable FACTORIO_USERNAME is not set');
@@ -135,10 +162,12 @@ export class ModManager {
135
162
  }
136
163
  let isOutdated = false;
137
164
  const targetPath = this.getModPath(modName);
165
+ this.log(`Using path ${targetPath} for ${modName}`);
138
166
  if (existsSync(targetPath) && targetVersion) {
139
167
  const modInfo = getModInfo(targetPath);
140
168
  if (modInfo.version != targetVersion) {
141
169
  isOutdated = true;
170
+ this.log(`Mod ${modName} is outdated`);
142
171
  }
143
172
  }
144
173
  await this.installZipMod(modName, zipPath, { ...options, clearCache: isOutdated ? true : options?.clearCache });
@@ -146,10 +175,17 @@ export class ModManager {
146
175
  const dependencies = getDependencies(this.getModPath(modName));
147
176
  for (let dependency of dependencies.required) {
148
177
  if (!vanillaMods.includes(dependency) && !options?.omitDependencies?.includes(dependency)) {
178
+ this.log(`Installing dependency ${dependency} for ${modName}`);
149
179
  await this.installPortalMod(dependency, { ...options, version: undefined });
150
180
  }
181
+ else {
182
+ this.log(`Not necessary to install dependency ${dependency} for ${modName}`);
183
+ }
151
184
  }
152
185
  }
186
+ else {
187
+ this.log(`Skipping dependencies for ${modName}`);
188
+ }
153
189
  }
154
190
  async getModInfo(modName) {
155
191
  const response = await fetch(`https://mods.factorio.com/api/mods/${modName}`);
@@ -163,16 +199,24 @@ export class ModManager {
163
199
  */
164
200
  async installZipMod(modName, sourcePath, options) {
165
201
  const targetPath = this.getModPath(modName);
202
+ this.log(`Installing ${modName} as zip from ${sourcePath} to ${targetPath}`);
166
203
  if (existsSync(targetPath) && options?.clearCache) {
204
+ this.log(`Deleting existing mod ${modName} at ${targetPath}`);
167
205
  rmSync(targetPath, { recursive: true });
168
206
  }
169
207
  if (!existsSync(targetPath)) {
170
208
  const unzipDir = path.join(this.workingDir, 'unzip');
209
+ this.log(`Unzipping ${modName} from ${sourcePath} to ${unzipDir}`);
171
210
  mkdirSync(unzipDir, { recursive: true });
172
211
  await extractZip(sourcePath, { dir: unzipDir });
173
212
  // Mod contents are in a single dir inside the zip file, and the dir can have any name so we need to handle that
174
- let dirs = readdirSync(unzipDir);
175
- renameSync(path.join(unzipDir, dirs[0]), targetPath);
213
+ const dirs = readdirSync(unzipDir);
214
+ const sourceDir = path.join(unzipDir, dirs[0]);
215
+ this.log(`Renaming ${sourceDir} to ${targetPath} for ${modName}`);
216
+ renameSync(sourceDir, targetPath);
217
+ }
218
+ else {
219
+ this.log(`Mod zip ${modName} already exists at ${targetPath}, no action needed`);
176
220
  }
177
221
  }
178
222
  getModPath(modName) {