synthetic-factorio 0.0.1 → 0.2.4
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 +47 -45
- package/dist/ModManager.d.ts +6 -0
- package/dist/ModManager.js +46 -2
- package/package.json +2 -1
- package/util_scripts/data_setup.lua +17 -17
- package/util_scripts/require_handler.lua +62 -62
package/README.md
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
# Synthetic Factorio
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// install mod
|
|
14
|
-
await manager.
|
|
15
|
-
// install
|
|
16
|
-
manager.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
engine.
|
|
27
|
-
//
|
|
28
|
-
engine.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
import { ModManager, FactorioEngine } from 'synthetic-factorio';
|
|
11
|
+
|
|
12
|
+
const manager = new ModManager();
|
|
13
|
+
// install vanilla "mod" data from https://github.com/wube/factorio-data (requires git CLI)
|
|
14
|
+
await manager.installVanillaMods();
|
|
15
|
+
// install mod by name from mod portal (requires FACTORIO_USERNAME and FACTORIO_TOKEN env variables)
|
|
16
|
+
await manager.installPortalMod('SimpleSeablock');
|
|
17
|
+
// install a mod in development from local files
|
|
18
|
+
manager.installDirectoryMod('my-mod', './my-mod-files', { clearCache: true });
|
|
19
|
+
|
|
20
|
+
// initialize the synthetic factorio engine and specify the mods that should be active
|
|
21
|
+
// vanilla "mods" need to be specified, including "base", however "core" does not need to be listed
|
|
22
|
+
const engine = new FactorioEngine({
|
|
23
|
+
mods: ['base', 'space-age', 'quality', 'recycler', 'elevated-rails', 'my-mod']
|
|
24
|
+
});
|
|
25
|
+
// run all mods through their settings phase
|
|
26
|
+
engine.runSettingsPhase();
|
|
27
|
+
// customize settings for your specific test run
|
|
28
|
+
engine.setSettings({ startup: { 'my-mod-setting': true } });
|
|
29
|
+
// run all mods through their data phase
|
|
30
|
+
engine.runDataPhase();
|
|
31
|
+
|
|
32
|
+
// read the data.raw collection and run test assertions
|
|
33
|
+
console.log(engine.getRawData()['item']['stone-furnace']);
|
|
34
|
+
|
|
35
|
+
// run control phase - NOTE: control phase support is currently very limited
|
|
36
|
+
engine.runControlPhase(['my-mod']);
|
|
37
|
+
|
|
38
|
+
// run any registered on_init callbacks
|
|
39
|
+
engine.triggerInit();
|
|
40
|
+
|
|
41
|
+
// trigger an event with the payload of your choice
|
|
42
|
+
// Partial on the generic and casts on the sub-properties are recommended to avoid the need to setup the entire event payload
|
|
43
|
+
engine.triggerEvent<Partial<runtime.on_chunk_generated>>(defines.events.on_chunk_generated, {
|
|
44
|
+
position: { x: 0, y: 0 },
|
|
45
|
+
surface: { name: 'nauvis' } as runtime.LuaSurface,
|
|
46
|
+
});
|
|
47
|
+
```
|
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) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synthetic-factorio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Synthetic Factorio runtime for unit testing",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Spencer Guest",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"repository": "https://github.com/sguest/synthetic-factorio",
|
|
8
9
|
"scripts": {
|
|
9
10
|
"test": "vitest",
|
|
10
11
|
"test:ci": "vitest run",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
-- define data and its extend and raw methods in lua, JS will later read this global to extract it
|
|
2
|
-
-- and may inject into data.raw to pre-populate the table
|
|
3
|
-
-- this is handled in lua instead of a JS callback for performance reasons
|
|
4
|
-
-- thousands of data.extend calls doing interop into JS is unusably slow
|
|
5
|
-
|
|
6
|
-
data = data or {}
|
|
7
|
-
data.raw = data.raw or {}
|
|
8
|
-
|
|
9
|
-
function data.extend(self, values)
|
|
10
|
-
for i, value in ipairs(values) do
|
|
11
|
-
-- space-age tries to extend on the string "./data/__space-age__/sound/ambient/space/interlude-6/interlude-6.lua"
|
|
12
|
-
-- (i.e. not a valid prototype object, just that string) so we need to gracefully handle invalid objects
|
|
13
|
-
if(value.name ~= nil and value.type ~= nil) then
|
|
14
|
-
data.raw[value.type] = data.raw[value.type] or {}
|
|
15
|
-
data.raw[value.type][value.name] = value
|
|
16
|
-
end
|
|
17
|
-
end
|
|
1
|
+
-- define data and its extend and raw methods in lua, JS will later read this global to extract it
|
|
2
|
+
-- and may inject into data.raw to pre-populate the table
|
|
3
|
+
-- this is handled in lua instead of a JS callback for performance reasons
|
|
4
|
+
-- thousands of data.extend calls doing interop into JS is unusably slow
|
|
5
|
+
|
|
6
|
+
data = data or {}
|
|
7
|
+
data.raw = data.raw or {}
|
|
8
|
+
|
|
9
|
+
function data.extend(self, values)
|
|
10
|
+
for i, value in ipairs(values) do
|
|
11
|
+
-- space-age tries to extend on the string "./data/__space-age__/sound/ambient/space/interlude-6/interlude-6.lua"
|
|
12
|
+
-- (i.e. not a valid prototype object, just that string) so we need to gracefully handle invalid objects
|
|
13
|
+
if(value.name ~= nil and value.type ~= nil) then
|
|
14
|
+
data.raw[value.type] = data.raw[value.type] or {}
|
|
15
|
+
data.raw[value.type][value.name] = value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
18
|
end
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
local old_require = require
|
|
2
|
-
|
|
3
|
-
-- Wrapper around lua's require() to handle several Factorio-isms that are used by either vanilla or other mods that break under regular lua import rules
|
|
4
|
-
|
|
5
|
-
-- Mods can require() the same filename across different mods, and each one will load its own version
|
|
6
|
-
-- for example both mods base and space-age import "prototypes.tile.tiles" but refer to different files
|
|
7
|
-
-- The lua spec says such an import re-uses the existing module, even if package.paths has changed, so this is a workaround
|
|
8
|
-
-- Each mod starts in its own "context" defined by the global current_mod_root_path
|
|
9
|
-
-- Imports are first attempted from this root path, and then allowed to fall back on error to find things like lualib
|
|
10
|
-
-- If a mod explicitly imports from another mod using the double-underscore syntax, i.e. require("__modname__.file")
|
|
11
|
-
-- then current_mod_root_path is set to that mod, and then restored after
|
|
12
|
-
|
|
13
|
-
-- This cannot be handled by running mods in different lua contexts because mods can re-use globals that were previously declared
|
|
14
|
-
-- for example "apply_heat_pipe_glow" is defined in base.prototypes.entity.entities, but then used in space-age.prototypes.entity.entities without being re-declared
|
|
15
|
-
-- Therefore both mods need to load in the same lua context to preserve globals
|
|
16
|
-
|
|
17
|
-
function require(name, ...)
|
|
18
|
-
local mod_changed = false
|
|
19
|
-
local previous_mod = current_mod_root_path
|
|
20
|
-
local new_name = name
|
|
21
|
-
if(string.find(name, '__')) then
|
|
22
|
-
mod_changed = true
|
|
23
|
-
local end_index = string.find(name, '__', 3)
|
|
24
|
-
current_mod_root_path = string.sub(name, 3, end_index - 1)
|
|
25
|
-
else
|
|
26
|
-
new_name = '__' .. current_mod_root_path .. '__.' .. name
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
-- This is the first load attempt that tries within the context of current_mod_root_path
|
|
30
|
-
local status, lib = pcall(old_require, new_name, ...)
|
|
31
|
-
if(mod_changed) then
|
|
32
|
-
current_mod_root_path = previous_mod
|
|
33
|
-
end
|
|
34
|
-
if(status) then return lib end
|
|
35
|
-
|
|
36
|
-
if(not string.find(lib, string.format("module '%s' not found", new_name), 1, true)) then
|
|
37
|
-
error(lib)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
-- First import has failed, fall back to the exact name that was provided. Most commonly this is loading lualib
|
|
41
|
-
-- though it also handles the case where mods require paths from vanilla mods without specifying the mod prefix
|
|
42
|
-
status, lib = pcall(old_require, name, ...)
|
|
43
|
-
if(status) then return lib end
|
|
44
|
-
if(not string.find(lib, string.format("module '%s' not found", name), 1, true)) then
|
|
45
|
-
error(lib)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
-- The factorio-data repo (https://github.com/wube/factorio-data) is the source for the data for the vanilla "mods"
|
|
49
|
-
-- This repo doesn't have things like graphics, sounds or menu simulations, but these are still require()d in many places from the existing lua files
|
|
50
|
-
-- The global suppress_require_errors is set during vanilla mod loading to suppress these
|
|
51
|
-
-- They are also suppressed when loading from the vanilla mod namespaces to handle loads from other mods
|
|
52
|
-
-- By default non-vanilla mods do not suppress, set the global suppress_require_errors = true to change
|
|
53
|
-
if(suppress_require_errors or string.find(name, '__base__') or string.find(name, '__space-age__', 1, true) or string.find(name, '__quality__') or string.find(name, '__recycler__') or string.find(name, '__elevated-rails__', 1, true)) then
|
|
54
|
-
if(string.find(name, 'graphics')) then
|
|
55
|
-
return { width = 1, height = 1, shift = util.by_pixel(1.0, 1.0), line_length = 5 }
|
|
56
|
-
end
|
|
57
|
-
if(string.find(name, 'menu-simulations', 1, true) or string.find(name, 'sound')) then
|
|
58
|
-
return {}
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
error(lib)
|
|
1
|
+
local old_require = require
|
|
2
|
+
|
|
3
|
+
-- Wrapper around lua's require() to handle several Factorio-isms that are used by either vanilla or other mods that break under regular lua import rules
|
|
4
|
+
|
|
5
|
+
-- Mods can require() the same filename across different mods, and each one will load its own version
|
|
6
|
+
-- for example both mods base and space-age import "prototypes.tile.tiles" but refer to different files
|
|
7
|
+
-- The lua spec says such an import re-uses the existing module, even if package.paths has changed, so this is a workaround
|
|
8
|
+
-- Each mod starts in its own "context" defined by the global current_mod_root_path
|
|
9
|
+
-- Imports are first attempted from this root path, and then allowed to fall back on error to find things like lualib
|
|
10
|
+
-- If a mod explicitly imports from another mod using the double-underscore syntax, i.e. require("__modname__.file")
|
|
11
|
+
-- then current_mod_root_path is set to that mod, and then restored after
|
|
12
|
+
|
|
13
|
+
-- This cannot be handled by running mods in different lua contexts because mods can re-use globals that were previously declared
|
|
14
|
+
-- for example "apply_heat_pipe_glow" is defined in base.prototypes.entity.entities, but then used in space-age.prototypes.entity.entities without being re-declared
|
|
15
|
+
-- Therefore both mods need to load in the same lua context to preserve globals
|
|
16
|
+
|
|
17
|
+
function require(name, ...)
|
|
18
|
+
local mod_changed = false
|
|
19
|
+
local previous_mod = current_mod_root_path
|
|
20
|
+
local new_name = name
|
|
21
|
+
if(string.find(name, '__')) then
|
|
22
|
+
mod_changed = true
|
|
23
|
+
local end_index = string.find(name, '__', 3)
|
|
24
|
+
current_mod_root_path = string.sub(name, 3, end_index - 1)
|
|
25
|
+
else
|
|
26
|
+
new_name = '__' .. current_mod_root_path .. '__.' .. name
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
-- This is the first load attempt that tries within the context of current_mod_root_path
|
|
30
|
+
local status, lib = pcall(old_require, new_name, ...)
|
|
31
|
+
if(mod_changed) then
|
|
32
|
+
current_mod_root_path = previous_mod
|
|
33
|
+
end
|
|
34
|
+
if(status) then return lib end
|
|
35
|
+
|
|
36
|
+
if(not string.find(lib, string.format("module '%s' not found", new_name), 1, true)) then
|
|
37
|
+
error(lib)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
-- First import has failed, fall back to the exact name that was provided. Most commonly this is loading lualib
|
|
41
|
+
-- though it also handles the case where mods require paths from vanilla mods without specifying the mod prefix
|
|
42
|
+
status, lib = pcall(old_require, name, ...)
|
|
43
|
+
if(status) then return lib end
|
|
44
|
+
if(not string.find(lib, string.format("module '%s' not found", name), 1, true)) then
|
|
45
|
+
error(lib)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
-- The factorio-data repo (https://github.com/wube/factorio-data) is the source for the data for the vanilla "mods"
|
|
49
|
+
-- This repo doesn't have things like graphics, sounds or menu simulations, but these are still require()d in many places from the existing lua files
|
|
50
|
+
-- The global suppress_require_errors is set during vanilla mod loading to suppress these
|
|
51
|
+
-- They are also suppressed when loading from the vanilla mod namespaces to handle loads from other mods
|
|
52
|
+
-- By default non-vanilla mods do not suppress, set the global suppress_require_errors = true to change
|
|
53
|
+
if(suppress_require_errors or string.find(name, '__base__') or string.find(name, '__space-age__', 1, true) or string.find(name, '__quality__') or string.find(name, '__recycler__') or string.find(name, '__elevated-rails__', 1, true)) then
|
|
54
|
+
if(string.find(name, 'graphics')) then
|
|
55
|
+
return { width = 1, height = 1, shift = util.by_pixel(1.0, 1.0), line_length = 5 }
|
|
56
|
+
end
|
|
57
|
+
if(string.find(name, 'menu-simulations', 1, true) or string.find(name, 'sound')) then
|
|
58
|
+
return {}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
error(lib)
|
|
63
63
|
end
|