diff --git a/.eslintrc.js b/.eslintrc.js index d4cbf768..0ff9374b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -320,7 +320,7 @@ module.exports = { "semi-style": "off", "sort-imports": "off", "sort-keys": "off", - "sort-vars": "error", + "sort-vars": "off", "space-before-blocks": "error", "space-before-function-paren": "off", "space-in-parens": ["error", "never"], diff --git a/CHANGELOG.md b/CHANGELOG.md index cac58b2a..485f1130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Fixed Web UI not showing instance saves when the permission to list and subscribe the saves is granted but the user is not a cluster admin. - Similarly fixed Web UI not showing the mod pack create, delete, list and host revoke token actions if the user is not a cluster admin. - Fixed Research Sync applying progress of a previous techology level to the level on the controller when it's the active research during instance startup. +- Fixed data export with a mod using shorthand notation for the default value of a color-setting causing the ModPack data to be set to invalid data. +- Fixed per user per instance player stats not being deleted when the instance it's for is deleted. +- Fixed mods setting the item prototype icons property to an object mapping instead of an array breaking the data export. +- Fixed a bad mod in the host's mods folder causing instance start to fail instead of the host re-downloading the mod. ## Version 2.0.0-alpha.16 diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 7bd67a52..4c98efc3 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -964,6 +964,7 @@ export default class Controller { this.instancesDirty = true; this.clearSavesOfInstance(instanceId); + this.userManager.clearStatsOfInstance(instanceId); let prev = instance.status; instance.status = "deleted"; diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index bb3cde0a..e9239428 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -113,4 +113,13 @@ export default class UserManager { this.onlineUsers.delete(user); } } + + clearStatsOfInstance(instanceId: number) { + for (const user of this.users.values()) { + this.notifyLeave(user, instanceId); + user.instanceStats.delete(instanceId); + user.recalculatePlayerStats(); + } + this.dirty = true; + } } diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index c2fc5d67..75e80ffa 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -600,8 +600,9 @@ export default class Host extends lib.Link { try { modInfo = await this.modStore.loadMod(mod.name, mod.version); } catch (err: any) { + // A broken zip file may have been left in the mods folder if (err.code !== "ENOENT") { - throw err; + logger.error(`Error loading mod ${mod.name} ${mod.version}: ${err.message}`); } } if (modInfo && (!mod.sha1 || mod.sha1 === modInfo.sha1)) { diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 938836d4..530f8fc9 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -18,11 +18,10 @@ interface SimpleIconSpecification { icon_size: number; icon_mipmaps?: number; } -type Color = { r: number, g: number, b: number, a: number }; interface IconLayer { icon: string; icon_size: number; - tint?: Color, + tint?: lib.FactorioColor, shift?: [number, number], scale?: number, icon_mipmaps?: number, @@ -202,15 +201,9 @@ async function loadLayeredIcon( iconLayer = iconLayer.clone(); - let tint: Color; + let tint: lib.Color; if (layer.tint) { - let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1; - tint = { - r: (layer.tint.r || 0) / divisor, - g: (layer.tint.g || 0) / divisor, - b: (layer.tint.b || 0) / divisor, - a: layer.tint.a || 1, - }; + tint = lib.normalizeColor(layer.tint); } else { tint = { r: 1, b: 1, g: 1, a: 1 }; } @@ -252,6 +245,18 @@ async function loadLayeredIcon( return icon; } +function fixIcons(item: ItemPrototype) { + const icons = item.icons; + if (typeof icons === "object" && !(icons instanceof Array) && icons !== null) { + // It's possible to specify icons as an object with arbitrary keys + // and the game will still accept it, cast the value to array if + // this is the case. + item = { ...item }; + item.icons = Object.values(icons); + } + return item; +} + const itemTypes = new Set([ "item", "ammo", @@ -284,7 +289,8 @@ const itemTypes = new Set([ function filterItems(prototypes: Prototypes): ItemPrototype[] { return Object.entries(prototypes) .filter(([type, _]) => itemTypes.has(type)) - .flatMap(([_, typePrototypes]) => Object.values(typePrototypes)) as ItemPrototype[] + .flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as ItemPrototype[]) + .map(fixIcons) ; } diff --git a/packages/lib/browser.ts b/packages/lib/browser.ts index 5c430809..97525776 100644 --- a/packages/lib/browser.ts +++ b/packages/lib/browser.ts @@ -6,6 +6,7 @@ export * from "./src/factorio/exchange_string"; export * from "./src/helpers"; export * from "./src/ini"; export * from "./src/link"; +export * from "./src/lua_tools"; export * from "./src/logging"; export * from "./src/permissions"; export * from "./src/plugin"; diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 094addb4..85dba1fb 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -2,6 +2,7 @@ import zlib from "zlib"; import { Type, Static } from "@sinclair/typebox"; import * as libSchema from "../schema"; +import * as libLuaTools from "../lua_tools"; import ExportManifest from "./ExportManifest"; import { integerFactorioVersion } from "./version"; @@ -363,7 +364,11 @@ export default class ModPack { continue; } - this.settings[settingType].set(prototype.name, { value: prototype.default_value }); + let value = prototype.default_value; + if (prototype.type === "color-setting") { + value = libLuaTools.normalizeColor(value); + } + this.settings[settingType].set(prototype.name, { value }); } } } diff --git a/packages/lib/src/lua_tools.ts b/packages/lib/src/lua_tools.ts index b9dbba13..007f9a16 100644 --- a/packages/lib/src/lua_tools.ts +++ b/packages/lib/src/lua_tools.ts @@ -3,6 +3,54 @@ * @module lib/lua_tools */ +/** + * A colour as it may originate from Factorio Lua code, see + * https://lua-api.factorio.com/latest/types/Color.html + */ +export type FactorioColor = { r?: number, g?: number, b?: number, a?: number } | [number, number, number, number?]; + +export type Color = { r: number, g: number, b: number, a: number }; + +/** + * Normalise a color as it may appear in Factorio Lua code to be an object + * with r, g, b, a properties in the range 0 to 1. + * + * @param color - + * A colour that may be specified with left out components or as an + * array and have the range 1 to 0 or 255 to 0. + * @returns + * Normalised colour where all 4 components are present as properties in + * the range 0 to 1. + */ +export function normalizeColor(color: FactorioColor) { + let r, g, b, a; + if (color instanceof Array) { + ([r, g, b, a] = color); + } else { + ({ r, g, b, a } = color); + } + + r ??= NaN; + g ??= NaN; + b ??= NaN; + a ??= NaN; + + // Note NaN > 1 is false and NaN / 255 is NaN + if (r > 1 || g > 1 || b > 1 || a > 1) { + r /= 255; + g /= 255; + b /= 255; + a /= 255; + } + + return { + r: Number.isNaN(r) ? 0 : r, + g: Number.isNaN(g) ? 0 : g, + b: Number.isNaN(b) ? 0 : b, + a: Number.isNaN(a) ? 1 : a, + }; +} + /** * Escapes a string for inclusion into a lua string * diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index a53b11d7..e3c20c4e 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -12,6 +12,7 @@ "./src/hash.ts", "./src/ini.ts", "./src/link/*.ts", + "./src/lua_tools.ts", "./src/logging.ts", "./src/permissions.ts", "./src/prometheus.ts", diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 97f13d03..840155f2 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -202,7 +202,7 @@ function SearchModsTable(props: SearchModsTableProps) { align: "right", render: (_, result) =>