mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d97b69b07 | ||
|
|
5b4621a7de |
No files matched your search
@@ -146,7 +146,7 @@ Your instances (save files etc) will be stored there.
|
|||||||
|
|
||||||
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
Make sure to note down the admin authentication token it provides at the end as you will need it later.
|
||||||
|
|
||||||
3. If you chose to use local factorio directory for the Factorio installation, download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder.
|
3. If you chose to use the local `factorio` directory for the Factorio installation, you can let the installer download the latest headless release automatically, or manually download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder.
|
||||||
|
|
||||||
|
|
||||||
### MacOS Setup
|
### MacOS Setup
|
||||||
|
|||||||
+110
-16
@@ -43,6 +43,9 @@ const availablePlugins = [
|
|||||||
{ name: "Server Select", value: "@hornwitser/server_select" },
|
{ name: "Server Select", value: "@hornwitser/server_select" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const windowsHeadlessDownloadUrl = "https://factorio-repackager.fly.dev/get-download/latest/headless/win64";
|
||||||
|
const headlessDownloadPlatforms = new Set(["linux", "win32"]);
|
||||||
|
|
||||||
const factorioLocations = {
|
const factorioLocations = {
|
||||||
win32: [
|
win32: [
|
||||||
"C:\\Program Files\\Factorio",
|
"C:\\Program Files\\Factorio",
|
||||||
@@ -53,6 +56,10 @@ const factorioLocations = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function supportsHeadlessDownload(platform = process.platform) {
|
||||||
|
return headlessDownloadPlatforms.has(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LineSplitter extends stream.Transform {
|
class LineSplitter extends stream.Transform {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
@@ -647,14 +654,14 @@ async function inquirerMissingArgs(args) {
|
|||||||
foundLocations.push(location);
|
foundLocations.push(location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.platform === "linux") {
|
if (supportsHeadlessDownload()) {
|
||||||
if (args.hasOwnProperty("downloadHeadless")) { answers.downloadHeadless = args.downloadHeadless; }
|
if (args.hasOwnProperty("downloadHeadless")) { answers.downloadHeadless = args.downloadHeadless; }
|
||||||
|
|
||||||
answers = await inquirer.prompt([
|
answers = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
name: "downloadHeadless",
|
name: "downloadHeadless",
|
||||||
message: "(Linux only) Automatically download latest factorio release?",
|
message: "Automatically download latest headless Factorio release?",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
], answers);
|
], answers);
|
||||||
@@ -671,7 +678,7 @@ async function inquirerMissingArgs(args) {
|
|||||||
message: "Path to Factorio installation",
|
message: "Path to Factorio installation",
|
||||||
choices: [
|
choices: [
|
||||||
...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })),
|
...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })),
|
||||||
{ name: "Use local factorio directory, you must copy an installation to it", value: "factorio" },
|
{ name: "Use local factorio directory", value: "factorio" },
|
||||||
{ name: "Provide path manually", value: null },
|
{ name: "Provide path manually", value: null },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -801,6 +808,87 @@ async function downloadLinuxServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFactorioVersion(factorioDir) {
|
||||||
|
const changelogPath = path.join(factorioDir, "data", "changelog.txt");
|
||||||
|
const changelog = await fs.readFile(changelogPath, "utf8");
|
||||||
|
for (const line of changelog.split(/\r?\n/)) {
|
||||||
|
const index = line.indexOf(":");
|
||||||
|
if (index === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.slice(0, index).trim().toLowerCase() === "version") {
|
||||||
|
return line.slice(index + 1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to detect Factorio version from ${changelogPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findFactorioRoot(rootDir) {
|
||||||
|
const entries = await fs.readdir(rootDir);
|
||||||
|
const factorioRoot = entries.find(entry => entry === "factorio" || entry === "Factorio")
|
||||||
|
?? (entries.length === 1 ? entries[0] : null);
|
||||||
|
if (!factorioRoot) {
|
||||||
|
throw new Error(`Unable to locate extracted Factorio directory in ${rootDir}`);
|
||||||
|
}
|
||||||
|
return path.join(rootDir, factorioRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadWindowsServer() {
|
||||||
|
let res = await fetch(windowsHeadlessDownloadUrl);
|
||||||
|
if (res.status === 202) {
|
||||||
|
throw new Error("Latest Windows headless package is not ready yet, try again in a few minutes.");
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch ${windowsHeadlessDownloadUrl}: HTTP ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
if (!res.body) {
|
||||||
|
throw new Error("Windows headless download did not include an archive payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpDir = path.join("temp", "create-temp");
|
||||||
|
const archivePath = path.join(tmpDir, "factorio-headless-win64.zip");
|
||||||
|
const tmpArchivePath = `${archivePath}.tmp`;
|
||||||
|
const tmpExtractDir = path.join(tmpDir, "win64-extract");
|
||||||
|
|
||||||
|
await fs.emptyDir(tmpDir);
|
||||||
|
|
||||||
|
logger.info("Downloading latest Windows headless release. This may take a while.");
|
||||||
|
const writeStream = fs.createWriteStream(tmpArchivePath);
|
||||||
|
stream.Readable.fromWeb(res.body).pipe(writeStream);
|
||||||
|
await finished(writeStream);
|
||||||
|
await fs.rename(tmpArchivePath, archivePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expandArchiveCommand =
|
||||||
|
`Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' ` +
|
||||||
|
`-DestinationPath '${tmpExtractDir.replace(/'/g, "''")}' -Force`;
|
||||||
|
await execFile("powershell.exe", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
expandArchiveCommand,
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("error executing command - do you have PowerShell available?");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpFactorioDir = await findFactorioRoot(tmpExtractDir);
|
||||||
|
const version = await getFactorioVersion(tmpFactorioDir);
|
||||||
|
const factorioDir = path.join("factorio", version);
|
||||||
|
|
||||||
|
if (await fs.pathExists(factorioDir)) {
|
||||||
|
logger.warn(`setting downloadDir to ${factorioDir}, but not downloading because already existing`);
|
||||||
|
} else {
|
||||||
|
await fs.ensureDir("factorio");
|
||||||
|
await fs.rename(tmpFactorioDir, factorioDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let args = yargs
|
let args = yargs
|
||||||
.option("log-level", {
|
.option("log-level", {
|
||||||
@@ -824,7 +912,7 @@ async function main() {
|
|||||||
nargs: 1, describe: "HTTP port to listen on [standalone/controller]", type: "number",
|
nargs: 1, describe: "HTTP port to listen on [standalone/controller]", type: "number",
|
||||||
})
|
})
|
||||||
.option("download-headless", {
|
.option("download-headless", {
|
||||||
describe: "Download the latest headless release [standalone/controller] [linux only]",
|
describe: "Download the latest headless release [standalone/host]",
|
||||||
type: "boolean", nargs: 0,
|
type: "boolean", nargs: 0,
|
||||||
})
|
})
|
||||||
.option("host-name", {
|
.option("host-name", {
|
||||||
@@ -861,17 +949,17 @@ async function main() {
|
|||||||
;
|
;
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
args = args
|
args = args.option("allow-install-as-root", {
|
||||||
.option("allow-install-as-root", {
|
nargs: 0, describe: "(Linux only) Allow installing as root (not recommended)", type: "boolean",
|
||||||
nargs: 0, describe: "(Linux only) Allow installing as root (not recommended)", type: "boolean",
|
});
|
||||||
})
|
}
|
||||||
.option("download-headless", {
|
if (supportsHeadlessDownload()) {
|
||||||
nargs: 0,
|
args = args.option("download-headless", {
|
||||||
describe: "(Linux only) Automatically download and unpack the latest factorio release. " +
|
nargs: 0,
|
||||||
"Can be set to false using --no-download-headless.",
|
describe: "Automatically download and unpack the latest headless Factorio release. " +
|
||||||
type: "boolean",
|
"Can be set to false using --no-download-headless.",
|
||||||
})
|
type: "boolean",
|
||||||
;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
args = args.argv;
|
args = args.argv;
|
||||||
@@ -901,7 +989,13 @@ async function main() {
|
|||||||
if (answers.factorioDir !== "factorio") {
|
if (answers.factorioDir !== "factorio") {
|
||||||
throw new InstallError("--download-headless option requires --factorio-dir to be set to factorio");
|
throw new InstallError("--download-headless option requires --factorio-dir to be set to factorio");
|
||||||
}
|
}
|
||||||
await downloadLinuxServer();
|
if (process.platform === "linux") {
|
||||||
|
await downloadLinuxServer();
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
|
await downloadWindowsServer();
|
||||||
|
} else {
|
||||||
|
throw new InstallError("--download-headless is only supported on Linux and Windows");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
|
|||||||
+102
-36
@@ -19,7 +19,7 @@ const execFileAsync = util.promisify(child_process.execFile);
|
|||||||
* @param factorioDir - Path factorio dir containing data/changelog.txt.
|
* @param factorioDir - Path factorio dir containing data/changelog.txt.
|
||||||
*/
|
*/
|
||||||
async function getFactorioVersion(factorioDir: string) {
|
async function getFactorioVersion(factorioDir: string) {
|
||||||
let changelog;
|
let changelog: string;
|
||||||
const changelogPath = path.join(factorioDir, "data", "changelog.txt");
|
const changelogPath = path.join(factorioDir, "data", "changelog.txt");
|
||||||
try {
|
try {
|
||||||
changelog = await fs.readFile(changelogPath, "utf-8");
|
changelog = await fs.readFile(changelogPath, "utf-8");
|
||||||
@@ -106,34 +106,40 @@ async function listFactorioVersions(factorioDir: string) {
|
|||||||
* @throws Network error if status of request is not ok
|
* @throws Network error if status of request is not ok
|
||||||
*/
|
*/
|
||||||
async function downloadAndExtractZip(url: string, targetDir: string) {
|
async function downloadAndExtractZip(url: string, targetDir: string) {
|
||||||
const res = await fetch(url);
|
const res = await fetchDownload(url);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the ZIP with JSZip
|
// Load the ZIP with JSZip
|
||||||
const buffer = Buffer.from(await res.arrayBuffer());
|
const buffer = Buffer.from(await res.arrayBuffer());
|
||||||
const zip = await JSZip.loadAsync(buffer);
|
const zip = await JSZip.loadAsync(buffer);
|
||||||
|
const tmpTargetDir = `${targetDir}.tmp-extract`;
|
||||||
|
const stripRootDir = hasSingleArchiveRoot(Object.values(zip.files));
|
||||||
|
|
||||||
// Extract each file/folder
|
await fs.remove(tmpTargetDir);
|
||||||
await fs.ensureDir(targetDir);
|
await fs.ensureDir(tmpTargetDir);
|
||||||
await Promise.all(
|
try {
|
||||||
Object.values(zip.files).map(async (entry) => {
|
await Promise.all(
|
||||||
const parts = entry.name.split("/").filter(Boolean);
|
Object.values(zip.files).map(async (entry) => {
|
||||||
const strippedPath = parts.slice(1).join("/");
|
const parts = entry.name.split("/").filter(Boolean);
|
||||||
if (!strippedPath) {
|
const strippedParts = stripRootDir ? parts.slice(1) : parts;
|
||||||
return; // This copies the behaviour of tar --strip-components 1
|
if (!strippedParts.length) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const entryPath = path.join(targetDir, strippedPath);
|
const entryPath = validatedExtractPath(tmpTargetDir, strippedParts);
|
||||||
if (entry.dir) {
|
if (entry.dir) {
|
||||||
await fs.ensureDir(entryPath);
|
await fs.ensureDir(entryPath);
|
||||||
} else {
|
} else {
|
||||||
await fs.ensureDir(path.dirname(entryPath));
|
await fs.ensureDir(path.dirname(entryPath));
|
||||||
await fs.writeFile(entryPath, await entry.async("nodebuffer"));
|
await fs.writeFile(entryPath, await entry.async("nodebuffer"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
await fs.remove(targetDir);
|
||||||
|
await fs.rename(tmpTargetDir, targetDir);
|
||||||
|
} catch (err) {
|
||||||
|
await fs.remove(tmpTargetDir);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,10 +150,7 @@ async function downloadAndExtractZip(url: string, targetDir: string) {
|
|||||||
* @throws Network error if status of request is not ok
|
* @throws Network error if status of request is not ok
|
||||||
*/
|
*/
|
||||||
async function downloadAndExtractTar(url: string, targetDir: string) {
|
async function downloadAndExtractTar(url: string, targetDir: string) {
|
||||||
const res = await fetch(url);
|
const res = await fetchDownload(url);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the buffer to a temp file
|
// Write the buffer to a temp file
|
||||||
const tmpFilePath = `${targetDir}.tmp`;
|
const tmpFilePath = `${targetDir}.tmp`;
|
||||||
@@ -157,7 +160,9 @@ async function downloadAndExtractTar(url: string, targetDir: string) {
|
|||||||
|
|
||||||
// Execute tar CLI to extract
|
// Execute tar CLI to extract
|
||||||
try {
|
try {
|
||||||
await execFileAsync("tar", ["-xf", tmpFilePath, "-C", targetDir, "--strip-components", "1"]);
|
await execFileAsync("tar", [
|
||||||
|
"-xf", tarPath(tmpFilePath), "-C", tarPath(targetDir), "--strip-components", "1",
|
||||||
|
]);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code === "ENOENT") {
|
if (err.code === "ENOENT") {
|
||||||
throw new Error("error executing tar command, do you have 'xz-utils' installed?");
|
throw new Error("error executing tar command, do you have 'xz-utils' installed?");
|
||||||
@@ -169,6 +174,57 @@ async function downloadAndExtractTar(url: string, targetDir: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDownload(url: string) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (res.status === 202) {
|
||||||
|
let detail = "";
|
||||||
|
try {
|
||||||
|
detail = await res.text();
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
throw new Error(`Download is not ready yet for ${url}${detail ? `: ${detail}` : ""}`);
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tarPath(filePath: string) {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return filePath.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSingleArchiveRoot(entries: JSZip.JSZipObject[]) {
|
||||||
|
const rootNames = new Set(
|
||||||
|
entries
|
||||||
|
.map(entry => entry.name.split("/").filter(Boolean)[0])
|
||||||
|
.filter((value): value is string => Boolean(value))
|
||||||
|
);
|
||||||
|
return rootNames.size === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatedExtractPath(rootDir: string, parts: string[]) {
|
||||||
|
const entryPath = path.resolve(rootDir, ...parts);
|
||||||
|
const relative = path.relative(rootDir, entryPath);
|
||||||
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||||
|
throw new Error(`Refusing to extract unsafe ZIP entry ${parts.join("/")}`);
|
||||||
|
}
|
||||||
|
return entryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeadlessDownload(version: lib.ExternalFactorioVersion) {
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
return { url: version.headlessUrl, extract: downloadAndExtractTar };
|
||||||
|
}
|
||||||
|
if (process.platform === "win32" && version.windowsHeadlessUrl) {
|
||||||
|
return { url: version.windowsHeadlessUrl, extract: downloadAndExtractZip };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find factorio data directory of the given version
|
* Find factorio data directory of the given version
|
||||||
*
|
*
|
||||||
@@ -749,7 +805,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
|||||||
;
|
;
|
||||||
|
|
||||||
let type = line.subarray(channelEnd + 1, channelEnd + 2).toString("utf-8");
|
let type = line.subarray(channelEnd + 1, channelEnd + 2).toString("utf-8");
|
||||||
let content;
|
let content: unknown;
|
||||||
if (type === "j") {
|
if (type === "j") {
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(line.subarray(channelEnd + 2).toString("utf-8"));
|
content = JSON.parse(line.subarray(channelEnd + 2).toString("utf-8"));
|
||||||
@@ -905,7 +961,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newWhitelistJson;
|
let newWhitelistJson: unknown;
|
||||||
try {
|
try {
|
||||||
newWhitelistJson = await fs.readJSON(filePath);
|
newWhitelistJson = await fs.readJSON(filePath);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -985,8 +1041,8 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
|||||||
const installedVersions = await listFactorioVersions(this._factorioDir);
|
const installedVersions = await listFactorioVersions(this._factorioDir);
|
||||||
if (!installedVersions.versions.has(latestVersion.version)) {
|
if (!installedVersions.versions.has(latestVersion.version)) {
|
||||||
const v = latestVersion.version;
|
const v = latestVersion.version;
|
||||||
// Can't download if not linux or this is a direct install
|
const download = getHeadlessDownload(latestVersion);
|
||||||
if (installedVersions.direct || process.platform !== "linux") {
|
if (installedVersions.direct || !download) {
|
||||||
this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`);
|
this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -994,8 +1050,18 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
|||||||
// Download the newer version
|
// Download the newer version
|
||||||
this._logger.info(`A newer version of factorio is available (${v}), starting download...`);
|
this._logger.info(`A newer version of factorio is available (${v}), starting download...`);
|
||||||
const dst = path.join(this._factorioDir, latestVersion.version);
|
const dst = path.join(this._factorioDir, latestVersion.version);
|
||||||
await downloadAndExtractTar(latestVersion.headlessUrl, dst);
|
try {
|
||||||
this._logger.info(`Downloaded factorio version ${v}`);
|
await download.extract(download.url, dst);
|
||||||
|
this._logger.info(`Downloaded factorio version ${v}`);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (installedVersions.versions.size > 0) {
|
||||||
|
this._logger.warn(
|
||||||
|
`Failed to download factorio version ${v}, continuing with installed versions: ${err}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,7 +1117,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (this._unexpected.length === 0) {
|
} else if (this._unexpected.length === 0) {
|
||||||
let msg;
|
let msg: string;
|
||||||
if (code !== null) {
|
if (code !== null) {
|
||||||
msg = `Factorio server unexpectedly shut down with code ${code}`;
|
msg = `Factorio server unexpectedly shut down with code ${code}`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+9
-5
@@ -2,6 +2,7 @@ import { Static, Type } from "@sinclair/typebox";
|
|||||||
import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data/version";
|
import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data/version";
|
||||||
|
|
||||||
const ARCHIVE_URL = "https://factorio.com/download/archive/";
|
const ARCHIVE_URL = "https://factorio.com/download/archive/";
|
||||||
|
const WINDOWS_HEADLESS_BASE_URL = "https://factorio-repackager.fly.dev/get-download";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the parsed result of a factorio version
|
* Represents the parsed result of a factorio version
|
||||||
@@ -10,9 +11,10 @@ export const ExternalFactorioVersionSchema = Type.Object({
|
|||||||
stable: Type.Boolean(),
|
stable: Type.Boolean(),
|
||||||
version: FullVersionSchema,
|
version: FullVersionSchema,
|
||||||
headlessUrl: Type.String(),
|
headlessUrl: Type.String(),
|
||||||
|
windowsHeadlessUrl: Type.Optional(Type.String()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExternalFactorioVersion = Static<typeof ExternalFactorioVersionSchema>
|
export type ExternalFactorioVersion = Static<typeof ExternalFactorioVersionSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all factorio versions that support headless
|
* Fetch all factorio versions that support headless
|
||||||
@@ -21,7 +23,7 @@ export type ExternalFactorioVersion = Static<typeof ExternalFactorioVersionSchem
|
|||||||
* @throws Network errors and non-ok status responses
|
* @throws Network errors and non-ok status responses
|
||||||
*/
|
*/
|
||||||
export async function fetchFactorioVersions(): Promise<ExternalFactorioVersion[]> {
|
export async function fetchFactorioVersions(): Promise<ExternalFactorioVersion[]> {
|
||||||
let res;
|
let res: Response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetch(ARCHIVE_URL);
|
res = await fetch(ARCHIVE_URL);
|
||||||
@@ -48,11 +50,11 @@ function parseFactorioVersions(html: string): ExternalFactorioVersion[] {
|
|||||||
|
|
||||||
// Match <a class=" slot-button-inline version-button-stable " href="/download/archive/2.0.72">2.0.72</a>
|
// Match <a class=" slot-button-inline version-button-stable " href="/download/archive/2.0.72">2.0.72</a>
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const anchorRegex = /<a[^>]*class="[^"]*version-button-(?<kind>stable|experimental)[^"]*"[^>]*href="[^"]*\/download\/archive\/(?<version>\d+\.\d+.\d+)"[^>]*>/g;
|
const anchorRegex = /<a[^>]*class="[^"]*version-button-(?<kind>stable|experimental)[^"]*"[^>]*href="[^"]*\/download\/archive\/(?<version>\d+\.\d+\.\d+)"[^>]*>/g;
|
||||||
|
|
||||||
let match: RegExpExecArray | null;
|
let match = anchorRegex.exec(html);
|
||||||
const minimumMultiplayerVersion = integerFullVersion("0.12.35");
|
const minimumMultiplayerVersion = integerFullVersion("0.12.35");
|
||||||
while ((match = anchorRegex.exec(html)) !== null) {
|
while (match !== null) {
|
||||||
const kind = match.groups?.kind;
|
const kind = match.groups?.kind;
|
||||||
const version = match.groups?.version;
|
const version = match.groups?.version;
|
||||||
|
|
||||||
@@ -61,8 +63,10 @@ function parseFactorioVersions(html: string): ExternalFactorioVersion[] {
|
|||||||
version: version,
|
version: version,
|
||||||
stable: kind === "stable",
|
stable: kind === "stable",
|
||||||
headlessUrl: `https://www.factorio.com/get-download/${version}/headless/linux64`,
|
headlessUrl: `https://www.factorio.com/get-download/${version}/headless/linux64`,
|
||||||
|
windowsHeadlessUrl: `${WINDOWS_HEADLESS_BASE_URL}/${version}/headless/win64`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
match = anchorRegex.exec(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...versions.values()];
|
return [...versions.values()];
|
||||||
|
|||||||
+36
-12
@@ -364,10 +364,12 @@ describe("host/server", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "0.2.1",
|
version: "0.2.1",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: false,
|
stable: false,
|
||||||
version: "0.2.0",
|
version: "0.2.0",
|
||||||
headlessUrl: "test2",
|
headlessUrl: "test2",
|
||||||
|
windowsHeadlessUrl: "win-test2",
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
assert.equal(fetchCalledWith, null);
|
assert.equal(fetchCalledWith, null);
|
||||||
@@ -380,10 +382,12 @@ describe("host/server", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.1",
|
version: "0.1.1",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: false,
|
stable: false,
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
headlessUrl: "test2",
|
headlessUrl: "test2",
|
||||||
|
windowsHeadlessUrl: "win-test2",
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
assert.equal(fetchCalledWith, null);
|
assert.equal(fetchCalledWith, null);
|
||||||
@@ -395,21 +399,28 @@ describe("host/server", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.5",
|
version: "0.1.5",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.1",
|
version: "0.1.1",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: false,
|
stable: false,
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
headlessUrl: "test2",
|
headlessUrl: "test2",
|
||||||
|
windowsHeadlessUrl: "win-test2",
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
assert.equal(fetchCalledWith, null);
|
assert.equal(fetchCalledWith, null);
|
||||||
});
|
});
|
||||||
it("should do nothing when on windows", async function() {
|
it("should do attempt to download on windows", async function() {
|
||||||
let logLine = null;
|
let infoLine = null;
|
||||||
server._logger = { info: line => { logLine = line; } };
|
let warnLine = null;
|
||||||
|
server._logger = {
|
||||||
|
info: line => { infoLine = line; },
|
||||||
|
warn: line => { warnLine = line; },
|
||||||
|
};
|
||||||
Object.defineProperty(process, "platform", { value: "win32" });
|
Object.defineProperty(process, "platform", { value: "win32" });
|
||||||
|
|
||||||
server._factorioDir = path.join("test", "file", "factorio");
|
server._factorioDir = path.join("test", "file", "factorio");
|
||||||
@@ -418,44 +429,56 @@ describe("host/server", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.5",
|
version: "0.1.5",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.1",
|
version: "0.1.1",
|
||||||
headlessUrl: "test2",
|
headlessUrl: "test2",
|
||||||
|
windowsHeadlessUrl: "win-test2",
|
||||||
}, {
|
}, {
|
||||||
stable: false,
|
stable: false,
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
headlessUrl: "test3",
|
headlessUrl: "test3",
|
||||||
|
windowsHeadlessUrl: "win-test3",
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
assert.equal(fetchCalledWith, null);
|
assert.equal(fetchCalledWith, "win-test1");
|
||||||
assert.ok(logLine !== null);
|
assert.ok(infoLine !== null);
|
||||||
assert.ok(logLine.endsWith("but must be manually downloaded"));
|
assert.ok(infoLine.endsWith("starting download..."));
|
||||||
|
assert.ok(warnLine?.includes("continuing with installed versions"));
|
||||||
});
|
});
|
||||||
it("should do attempt to download on linux", async function() {
|
it("should do attempt to download on linux", async function() {
|
||||||
let logLine = null;
|
let infoLine = null;
|
||||||
server._logger = { info: line => { logLine = line; } };
|
let warnLine = null;
|
||||||
|
server._logger = {
|
||||||
|
info: line => { infoLine = line; },
|
||||||
|
warn: line => { warnLine = line; },
|
||||||
|
};
|
||||||
Object.defineProperty(process, "platform", { value: "linux" });
|
Object.defineProperty(process, "platform", { value: "linux" });
|
||||||
|
|
||||||
server._factorioDir = path.join("test", "file", "factorio");
|
server._factorioDir = path.join("test", "file", "factorio");
|
||||||
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
|
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
|
||||||
await assert.rejects(server.checkForUpdates([{
|
await server.checkForUpdates([{
|
||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.5",
|
version: "0.1.5",
|
||||||
headlessUrl: "test1",
|
headlessUrl: "test1",
|
||||||
|
windowsHeadlessUrl: "win-test1",
|
||||||
}, {
|
}, {
|
||||||
stable: true,
|
stable: true,
|
||||||
version: "0.1.1",
|
version: "0.1.1",
|
||||||
headlessUrl: "test2",
|
headlessUrl: "test2",
|
||||||
|
windowsHeadlessUrl: "win-test2",
|
||||||
}, {
|
}, {
|
||||||
stable: false,
|
stable: false,
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
headlessUrl: "test3",
|
headlessUrl: "test3",
|
||||||
}]), new Error("Failed to fetch test1: -1 Fetch called"));
|
windowsHeadlessUrl: "win-test3",
|
||||||
|
}]);
|
||||||
|
|
||||||
assert.equal(fetchCalledWith, "test1");
|
assert.equal(fetchCalledWith, "test1");
|
||||||
assert.ok(logLine !== null);
|
assert.ok(infoLine !== null);
|
||||||
assert.ok(logLine.endsWith("starting download..."));
|
assert.ok(infoLine.endsWith("starting download..."));
|
||||||
|
assert.ok(warnLine?.includes("continuing with installed versions"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
/* eslint-enable no-loop-func */
|
/* eslint-enable no-loop-func */
|
||||||
@@ -474,6 +497,7 @@ describe("host/server", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "2.0.73",
|
version: "2.0.73",
|
||||||
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
||||||
|
windowsHeadlessUrl: "https://factorio-repackager.fly.dev/get-download/2.0.73/headless/win64",
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+3
@@ -55,11 +55,13 @@ describe("FactorioVersions", function() {
|
|||||||
stable: false,
|
stable: false,
|
||||||
version: "2.0.73",
|
version: "2.0.73",
|
||||||
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
||||||
|
windowsHeadlessUrl: "https://factorio-repackager.fly.dev/get-download/2.0.73/headless/win64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stable: true,
|
stable: true,
|
||||||
version: "2.0.72",
|
version: "2.0.72",
|
||||||
headlessUrl: "https://www.factorio.com/get-download/2.0.72/headless/linux64",
|
headlessUrl: "https://www.factorio.com/get-download/2.0.72/headless/linux64",
|
||||||
|
windowsHeadlessUrl: "https://factorio-repackager.fly.dev/get-download/2.0.72/headless/win64",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -91,6 +93,7 @@ describe("FactorioVersions", function() {
|
|||||||
stable: true,
|
stable: true,
|
||||||
version: "2.0.73",
|
version: "2.0.73",
|
||||||
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
||||||
|
windowsHeadlessUrl: "https://factorio-repackager.fly.dev/get-download/2.0.73/headless/win64",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user