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 +49 -45
- package/dist/ModManager.d.ts +6 -0
- package/dist/ModManager.js +46 -2
- package/dist/defines.d.ts +1710 -0
- package/dist/defines.js +1710 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +4 -2
- package/util_scripts/data_setup.lua +21 -17
- package/util_scripts/defines.lua +332 -326
- package/util_scripts/require_handler.lua +62 -62
package/README.md
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
# Synthetic Factorio
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// install
|
|
16
|
-
manager.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
engine
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// run all mods through their
|
|
28
|
-
engine.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
engine.
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
```
|
package/dist/ModManager.d.ts
CHANGED
|
@@ -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
|
package/dist/ModManager.js
CHANGED
|
@@ -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
|
-
|
|
175
|
-
|
|
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) {
|