mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Merge pull request #952 from lntutor/feat/mod-info-recommendations-949
Show informational mod recommendations
This commit is contained in:
4 files changed
+213
-26
No files matched your search
@@ -12,7 +12,7 @@ import ModInfo, { ModDependencyUnsatisfiedReason } from "./ModInfo";
|
||||
|
||||
import {
|
||||
PartialVersion, PartialVersionSchema, integerPartialVersion,
|
||||
SourceVersion, SourceVersionSchema, normaliseFullVersion,
|
||||
SourceVersion, SourceVersionSchema, normaliseFullVersion, integerSourceVersion,
|
||||
} from "./version";
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ export interface ModSetting {
|
||||
value: boolean | number | string | ModSettingColor;
|
||||
}
|
||||
|
||||
export type ModRecordAdvisory =
|
||||
| { type: "recommended_dependency", sourceModName: string }
|
||||
| { type: "update_available", version: SourceVersion };
|
||||
|
||||
const ModSettingJsonSchema = Type.Object({
|
||||
"value": Type.Union([Type.Boolean(), Type.Number(), Type.String(), ModSettingColor]),
|
||||
});
|
||||
@@ -56,6 +60,86 @@ export interface ModRecord {
|
||||
warning?: ModDependencyUnsatisfiedReason | "wrong_factorio_version",
|
||||
/** Used inside packages\web_ui\src\components\ModPackViewPage.tsx when there is no error. */
|
||||
info?: ModInfo,
|
||||
/** Non-blocking recommendations displayed by the Web UI. */
|
||||
advisories?: ModRecordAdvisory[],
|
||||
}
|
||||
|
||||
type AnnotatedModRecord = ModRecord & { advisories: ModRecordAdvisory[] };
|
||||
|
||||
/**
|
||||
* Finds newer versions already installed on the controller for mods in a pack.
|
||||
*/
|
||||
export function getInstalledModUpdates(mods: ModRecord[], installedMods: Iterable<ModInfo>) {
|
||||
const updates = new Map<string, SourceVersion>();
|
||||
|
||||
for (const mod of mods) {
|
||||
if (!mod.info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const installedMod of installedMods) {
|
||||
if (
|
||||
installedMod.name !== mod.name
|
||||
|| installedMod.factorioVersion !== mod.info.factorioVersion
|
||||
|| installedMod.integerVersion <= integerSourceVersion(mod.version)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingUpdate = updates.get(mod.name);
|
||||
if (!existingUpdate || installedMod.integerVersion > integerSourceVersion(existingUpdate)) {
|
||||
updates.set(mod.name, installedMod.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
export function applyModRecordAdvisories(
|
||||
mods: ModRecord[],
|
||||
availableUpdates: ReadonlyMap<string, SourceVersion> = new Map(),
|
||||
) {
|
||||
const annotatedMods: AnnotatedModRecord[] = mods.map(mod => ({ ...mod, advisories: [] }));
|
||||
const modsByName = new Map(annotatedMods.map(mod => [mod.name, mod]));
|
||||
|
||||
for (const sourceMod of annotatedMods) {
|
||||
if (!sourceMod.enabled || !sourceMod.info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const dependency of sourceMod.info.dependencies) {
|
||||
if (!dependency.recommended) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetMod = modsByName.get(dependency.name);
|
||||
if (
|
||||
!targetMod
|
||||
|| targetMod.enabled
|
||||
|| dependency.version && !dependency.version.testVersion(targetMod.version)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targetMod.advisories.push({
|
||||
type: "recommended_dependency",
|
||||
sourceModName: sourceMod.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const mod of annotatedMods) {
|
||||
const availableVersion = availableUpdates.get(mod.name);
|
||||
if (availableVersion && integerSourceVersion(availableVersion) > integerSourceVersion(mod.version)) {
|
||||
mod.advisories.push({ type: "update_available", version: availableVersion });
|
||||
}
|
||||
if (mod.advisories.length === 0) {
|
||||
delete (mod as ModRecord).advisories;
|
||||
}
|
||||
}
|
||||
|
||||
return annotatedMods;
|
||||
}
|
||||
|
||||
const ModRecordJsonSchema = Type.Object({
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
export { default as ExportManifest } from "./ExportManifest";
|
||||
export { default as ModInfo, ModDependency } from "./ModInfo";
|
||||
export { ModSettingColor } from "./ModPack";
|
||||
export type { ModSetting, ModRecord } from "./ModPack";
|
||||
export { ModSettingColor, getInstalledModUpdates, applyModRecordAdvisories } from "./ModPack";
|
||||
export type { ModSetting, ModRecord, ModRecordAdvisory } from "./ModPack";
|
||||
export { default as ModPack } from "./ModPack";
|
||||
export { default as ModuleInfo } from "./ModuleInfo";
|
||||
export { default as Permission } from "./Permission";
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
ExportOutlined, FileUnknownOutlined, FileExclamationOutlined, FileSyncOutlined,
|
||||
CloseOutlined, DeleteOutlined, ToolOutlined, PlusOutlined, CloudSyncOutlined, CloudDownloadOutlined,
|
||||
LikeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import type { SorterResult, FilterValue, TableCurrentDataSource } from "antd/es/table/interface";
|
||||
@@ -545,7 +546,7 @@ function ModsTable(props: ModsTableProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const mods = [...props.modPack.mods.values(), ...deletedMods.values()].map(
|
||||
let mods = [...props.modPack.mods.values(), ...deletedMods.values()].map(
|
||||
(mod: lib.ModRecord): lib.ModRecord => {
|
||||
if (props.builtInModNames.includes(mod.name)) {
|
||||
return {
|
||||
@@ -590,6 +591,7 @@ function ModsTable(props: ModsTableProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
mods = lib.applyModRecordAdvisories(mods, lib.getInstalledModUpdates(mods, modInfos.values()));
|
||||
|
||||
async function fixDependencyIssues(mod: lib.ModRecord) {
|
||||
if (!mod.info) {
|
||||
@@ -766,27 +768,48 @@ function ModsTable(props: ModsTableProps) {
|
||||
{
|
||||
title: "Name",
|
||||
key: "name",
|
||||
render: (_, mod: lib.ModRecord) => <>
|
||||
{mod.error === "missing" && <Tooltip title="Mod is missing from storage.">
|
||||
<FileUnknownOutlined style={{ color: "#a61d24" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.error === "bad_checksum" && <Tooltip title="Mod checksum mismatch.">
|
||||
<FileExclamationOutlined style={{ color: "#a61d24" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "incompatible" && <Tooltip title="Mod is incompatible with another.">
|
||||
<FileExclamationOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "missing_dependency" && <Tooltip title="Mod is missing a dependency.">
|
||||
<FileUnknownOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "wrong_version" && <Tooltip title="Mod has wrong dependency version added.">
|
||||
<FileSyncOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "wrong_factorio_version" && <Tooltip title="Mod has wrong factorio version.">
|
||||
<FileSyncOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.info?.title || mod.name}
|
||||
</>,
|
||||
render: (_, mod: lib.ModRecord) => {
|
||||
const recommendedBy = mod.advisories
|
||||
?.filter(advisory => advisory.type === "recommended_dependency")
|
||||
.map(advisory => advisory.sourceModName) ?? [];
|
||||
const update = mod.advisories
|
||||
?.find(advisory => advisory.type === "update_available");
|
||||
return <>
|
||||
{mod.error === "missing" && <Tooltip title="Mod is missing from storage.">
|
||||
<FileUnknownOutlined style={{ color: "#a61d24" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.error === "bad_checksum" && <Tooltip title="Mod checksum mismatch.">
|
||||
<FileExclamationOutlined style={{ color: "#a61d24" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "incompatible" && <Tooltip title="Mod is incompatible with another.">
|
||||
<FileExclamationOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "missing_dependency" && <Tooltip title="Mod is missing a dependency.">
|
||||
<FileUnknownOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "wrong_version" && <Tooltip
|
||||
title="Mod has wrong dependency version added."
|
||||
>
|
||||
<FileSyncOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.warning === "wrong_factorio_version" && <Tooltip
|
||||
title="Mod has wrong factorio version."
|
||||
>
|
||||
<FileSyncOutlined style={{ color: "#dd5e14" }} />{" "}
|
||||
</Tooltip>}
|
||||
{recommendedBy.length > 0 && <Tooltip
|
||||
title={`Recommended by ${recommendedBy.join(", ")}, but currently disabled.`}
|
||||
>
|
||||
<LikeOutlined style={{ color: "#0958d9" }} />{" "}
|
||||
</Tooltip>}
|
||||
{update?.type === "update_available" && <Tooltip
|
||||
title={`A newer compatible version (${update.version}) is available.`}
|
||||
>
|
||||
<CloudSyncOutlined style={{ color: "#0958d9" }} />{" "}
|
||||
</Tooltip>}
|
||||
{mod.info?.title || mod.name}
|
||||
</>;
|
||||
},
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => strcmp(a.name, b.name),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ const assert = require("assert").strict;
|
||||
const zlib = require("zlib");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const { ModPack } = lib;
|
||||
const { ModInfo, ModPack, getInstalledModUpdates, applyModRecordAdvisories } = lib;
|
||||
|
||||
|
||||
describe("lib/data/ModPack", function() {
|
||||
@@ -421,4 +421,84 @@ describe("lib/data/ModPack", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyModRecordAdvisories()", function() {
|
||||
it("finds newer installed versions for the same Factorio version", function() {
|
||||
const current = ModInfo.fromJSON({
|
||||
name: "source",
|
||||
version: "1.0.0",
|
||||
factorio_version: "2.0",
|
||||
});
|
||||
const newer = ModInfo.fromJSON({
|
||||
name: "source",
|
||||
version: "1.1.0",
|
||||
factorio_version: "2.0",
|
||||
});
|
||||
const otherFactorioVersion = ModInfo.fromJSON({
|
||||
name: "source",
|
||||
version: "2.0.0",
|
||||
factorio_version: "1.1",
|
||||
});
|
||||
|
||||
const updates = getInstalledModUpdates([
|
||||
{ name: "source", enabled: true, version: "1.0.0", info: current },
|
||||
], [current, newer, otherFactorioVersion]);
|
||||
|
||||
assert.deepEqual([...updates], [["source", "1.1.0"]]);
|
||||
});
|
||||
|
||||
it("marks compatible disabled recommended dependencies", function() {
|
||||
const sourceInfo = ModInfo.fromJSON({
|
||||
name: "source",
|
||||
version: "1.0.0",
|
||||
dependencies: ["+ quality >= 2.0.0", "+ stale >= 2.0.0", "+ active"],
|
||||
});
|
||||
const otherInfo = ModInfo.fromJSON({
|
||||
name: "other",
|
||||
version: "1.0.0",
|
||||
dependencies: ["+ quality"],
|
||||
});
|
||||
const mods = [
|
||||
{ name: "source", enabled: true, version: "1.0.0", info: sourceInfo },
|
||||
{ name: "other", enabled: true, version: "1.0.0", info: otherInfo },
|
||||
{ name: "ignored", enabled: false, version: "1.0.0", info: otherInfo },
|
||||
{ name: "quality", enabled: false, version: "2.0.0" },
|
||||
{ name: "stale", enabled: false, version: "1.0.0" },
|
||||
{ name: "active", enabled: true, version: "1.0.0" },
|
||||
];
|
||||
|
||||
const annotated = applyModRecordAdvisories(mods);
|
||||
|
||||
assert.deepEqual(annotated[3].advisories, [
|
||||
{ type: "recommended_dependency", sourceModName: "source" },
|
||||
{ type: "recommended_dependency", sourceModName: "other" },
|
||||
]);
|
||||
assert.equal(annotated[4].advisories, undefined);
|
||||
assert.equal(annotated[5].advisories, undefined);
|
||||
assert.equal(mods[3].advisories, undefined);
|
||||
});
|
||||
|
||||
it("marks only newer resolved updates and preserves other advisories", function() {
|
||||
const sourceInfo = ModInfo.fromJSON({
|
||||
name: "source",
|
||||
version: "1.0.0",
|
||||
dependencies: ["+ quality"],
|
||||
});
|
||||
const mods = [
|
||||
{ name: "source", enabled: true, version: "1.0.0", info: sourceInfo },
|
||||
{ name: "quality", enabled: false, version: "2.0.0" },
|
||||
];
|
||||
|
||||
const annotated = applyModRecordAdvisories(mods, new Map([
|
||||
["source", "1.0.0"],
|
||||
["quality", "2.1.0"],
|
||||
]));
|
||||
|
||||
assert.deepEqual(annotated[0].advisories, undefined);
|
||||
assert.deepEqual(annotated[1].advisories, [
|
||||
{ type: "recommended_dependency", sourceModName: "source" },
|
||||
{ type: "update_available", version: "2.1.0" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user