From 72264f0c5309f4729f9a95b401318b130938b671 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 21 Jan 2024 23:48:26 +0100 Subject: [PATCH] Add suport for color mod setting (#568) Implement the color-setting type in ModPack and display a color picker for it in the Web UI. This type of setting was added in Factorio 1.1.77. --- CHANGELOG.md | 7 ++++ packages/ctl/src/commands.ts | 19 ++++++++- packages/lib/src/data/ModPack.ts | 14 +++++-- packages/lib/src/data/index.ts | 2 +- .../web_ui/src/components/ModPackViewPage.tsx | 33 +++++++++++++-- test/integration/clusterio.js | 12 ++++-- test/lib/data/ModPack.js | 40 ++++++++++++++++++- 7 files changed, 114 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ee3a6f..5013726a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Version 2.0.0-alpha.15 + +### Features + +- Added support for `color-settings` Mod settings (#568)[https://github.com/clusterio/clusterio/issues/568]. + + ## Version 2.0.0-alpha.14 This release does not retain backwards compatibility with 2.0.0-alpha.13 meaning all parts of the cluster needs to updated at the same time. diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index 8e892a50..76150cc3 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -1097,7 +1097,13 @@ modPackCommands.add(new lib.Command({ function setModPackSettings( modPack: lib.ModPack, - args: { boolSetting?: string[], intSetting?: string[], doubleSetting?: string[], stringSetting?: string[] }, + args: { + boolSetting?: string[], + intSetting?: string[], + doubleSetting?: string[], + stringSetting?: string[], + colorSetting?: string[], + }, ) { function doSettings(settings: string[], cast: (value: string) => (string | number | boolean)) { for (let i = 0; i + 2 < settings.length; i += 3) { @@ -1134,6 +1140,13 @@ function setModPackSettings( return number; }); doSettings(args.stringSetting || [], value => value); + doSettings(args.colorSetting || [], value => { + let color = JSON.parse(value); + if (typeof color !== "object" || color === null) { + throw new lib.CommandError("color value must be a JSON object with r, g, b, a properties"); + } + return color; + }); } function setModPackMods(modPack: lib.ModPack, mods: string[] | undefined) { @@ -1173,6 +1186,7 @@ modPackCommands.add(new lib.Command({ "int-setting": { describe: "Set int setting", array: true, nargs: 3, type: "string" }, "double-setting": { describe: "Set double setting", array: true, nargs: 3, type: "string" }, "string-setting": { describe: "Set string setting", array: true, nargs: 3, type: "string" }, + "color-setting": { describe: "Set color setting", array: true, nargs: 3, type: "string" }, }); }], handler: async function( @@ -1186,6 +1200,7 @@ modPackCommands.add(new lib.Command({ intSetting?: string[], doubleSetting?: string[], stringSetting?: string[], + colorSetting?: string[], }, control: Control, ) { @@ -1244,6 +1259,7 @@ modPackCommands.add(new lib.Command({ "int-setting": { describe: "Set int setting", array: true, nargs: 3, type: "string" }, "double-setting": { describe: "Set double setting", array: true, nargs: 3, type: "string" }, "string-setting": { describe: "Set string setting", array: true, nargs: 3, type: "string" }, + "color-setting": { describe: "Set color setting", array: true, nargs: 3, type: "string" }, "remove-setting": { describe: "Remove a setting", array: true, nargs: 2, type: "string" }, }); }], @@ -1261,6 +1277,7 @@ modPackCommands.add(new lib.Command({ intSetting?: string[], doubleSetting?: string[], stringSetting?: string[], + colorSetting?: string[], removeSetting?: string[], }, control: Control, diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 2aec7b63..094addb4 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -8,16 +8,24 @@ import { integerFactorioVersion } from "./version"; import type { Logger } from "../logging"; +export const ModSettingColor = Type.Object({ + r: Type.Number(), + g: Type.Number(), + b: Type.Number(), + a: Type.Number(), +}); +export type ModSettingColor = Static; + /** * A setting for a mod. */ export interface ModSetting { /** Value of the given mod setting. */ - value: boolean | number | string; + value: boolean | number | string | ModSettingColor; } const ModSettingJsonSchema = Type.Object({ - "value": Type.Union([Type.Boolean(), Type.Number(), Type.String()]), + "value": Type.Union([Type.Boolean(), Type.Number(), Type.String(), ModSettingColor]), }); const ModSettingsJsonSchema = Type.Record(Type.String(), ModSettingJsonSchema); @@ -335,7 +343,7 @@ export default class ModPack { * @param logger - Logger used to report warnings on. */ fillDefaultSettings(settingPrototypes: Record, logger: Logger) { - const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting"]; + const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting", "color-setting"]; let prototypes = Object.entries(settingPrototypes) .filter(([type, _]) => knownTypes.includes(type)) .flatMap(([_, settings]) => Object.values(settings)) diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index 86668cdc..0ca5f47a 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -5,7 +5,7 @@ */ export { default as ExportManifest } from "./ExportManifest"; export { default as ModInfo } from "./ModInfo"; -export { default as ModPack, ModSetting, ModRecord } from "./ModPack"; +export { default as ModPack, ModSetting, ModSettingColor, ModRecord } from "./ModPack"; export { default as ModuleInfo } from "./ModuleInfo"; export { default as Permission } from "./Permission"; export { default as PlayerStats } from "./PlayerStats"; diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index ae2d2821..47c86fa5 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -1,7 +1,7 @@ import React, { Fragment, memo, useCallback, useEffect, useContext, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { - Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, Pagination, + Button, Card, ColorPicker, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, Pagination, Popconfirm, Row, Table, Tag, Typography, Select, Skeleton, Space, Spin, Switch, Modal, Tooltip, TablePaginationConfig, } from "antd"; @@ -404,7 +404,7 @@ function hasChange(changes: ModChange[], field:Partial) { } // picks out the value property from settings. -function pickValue(map: Map): [string, string|number|boolean][] { +function pickValue(map: Map): [string, lib.ModSetting["value"]][] { return [...map].map(([name, value]) => [name, value.value]); } @@ -420,6 +420,25 @@ function groupToMap(array: any[], fn: (...args:any) => any) { return map; } +function InputColor(props: { + value: lib.ModSettingColor, + onChange: (value: lib.ModSettingColor) => void, +}) { + const value = props.value; + return { + const rgb = color.toRgb(); + props.onChange({ + r: rgb.r / 255, + g: rgb.g / 255, + b: rgb.b / 255, + a: rgb.a, + }); + }} + />; +} type SettingsTableFieldProps = { name: string; type: string; @@ -431,11 +450,15 @@ type SettingsTableFieldProps = { const SettingsTableField = memo((props: SettingsTableFieldProps) => { const name = props.name; const isBoolean = props.type === "bool-setting"; + const isColor = props.type === "color-setting"; const isNumber = ["int-setting", "double-setting"].includes(props.type); const isSelect = Boolean(props.allowedValues); let input; if (isBoolean) { input = ; + } else if (isColor) { + const PartialInputColor = InputColor as () => React.JSX.Element; + input = ; } else if (isSelect) { const options = Object.values(props.allowedValues).map(value => ({ value, @@ -484,7 +507,7 @@ function SettingsTable(props: SettingsTableProps) { .map(mod => [mod.name, mod.title]) ); - const types = ["bool-setting", "int-setting", "double-setting", "string-setting"]; + const types = ["bool-setting", "int-setting", "double-setting", "string-setting", "color-setting"]; let prototypes = Object.entries(props.prototypes) .filter(([type, _]) => types.includes(type)) .flatMap(([_, settingPrototypes]: [string, any]) => Object.values(settingPrototypes)) @@ -492,7 +515,7 @@ function SettingsTable(props: SettingsTableProps) { function controls( scope: "startup"|"runtime-global"|"runtime-per-user", - storedFields: [string, string|number|boolean][] + storedFields: [string, lib.ModSetting["value"]][] ) { let fields = new Map(prototypes .filter(p => p.setting_type === scope) @@ -508,6 +531,8 @@ function SettingsTable(props: SettingsTableProps) { type = "double-setting"; } else if (typeof value === "string") { type = "string-setting"; + } else if (typeof value === "object" && "r" in value) { + type = "color-setting"; } else { throw Error(`Unhandled setting type ${typeof value}`); } diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index b99e912f..dcb3c36d 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -81,6 +81,10 @@ async function getUsers() { return new Map(users.map(user => [user.name, user])); } +function jsonArg(value) { + return `"${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}"`; +} + describe("Integration of Clusterio", function() { describe("clusteriocontroller", function() { describe("bootstrap generate-user-token", function() { @@ -502,8 +506,7 @@ describe("Integration of Clusterio", function() { ]; for (let [prop, value] of testConfigs) { - value = `"${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}"`; - let args = `test factorio.settings ${prop} ${value}`; + let args = `test factorio.settings ${prop} ${jsonArg(value)}`; await execCtl(`instance config set-prop ${args}`); } @@ -876,6 +879,7 @@ describe("Integration of Clusterio", function() { assert(modPacks.some(modPack => modPack.name === "empty-pack"), "created pack is not in the list"); }); it("should allow setting all fields", async function() { + const color = { r: 1, g: 0, b: 1, a: 0 }; await execCtl( "mod-pack create full-pack 0.17.59 " + "--description Description " + @@ -883,7 +887,8 @@ describe("Integration of Clusterio", function() { "--bool-setting startup MyBool true " + "--int-setting runtime-global MyInt 1235 " + "--double-setting runtime-global MyDouble 12.25 " + - "--string-setting runtime-per-user MyString a-string" + "--string-setting runtime-per-user MyString a-string " + + `--color-setting runtime-per-user MyColor ${jsonArg(color)}` ); let modPacks = await getControl().send(new lib.ModPackListRequest()); let modPack = modPacks.find(entry => entry.name === "full-pack"); @@ -898,6 +903,7 @@ describe("Integration of Clusterio", function() { reference.settings["runtime-global"].set("MyInt", { value: 1235 }); reference.settings["runtime-global"].set("MyDouble", { value: 12.25 }); reference.settings["runtime-per-user"].set("MyString", { value: "a-string" }); + reference.settings["runtime-per-user"].set("MyColor", { value: color }); reference.updatedAtMs = modPack.updatedAtMs; assert.deepEqual(modPack, reference); }); diff --git a/test/lib/data/ModPack.js b/test/lib/data/ModPack.js index 10134b68..14616da3 100644 --- a/test/lib/data/ModPack.js +++ b/test/lib/data/ModPack.js @@ -39,6 +39,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string-setting": { "value": "a string" }, + "color-setting": { "value": { "r": 1, "g": 1, "b": 0, "a": 1 } }, }, }})); check(ModPack.fromJSON({ export_manifest: { assets: { setting: "settings.json" }}})); @@ -60,6 +61,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string-setting": { "value": "a string" }, + "color-setting": { "value": { "r": 1, "g": 1, "b": 0, "a": 1 } }, }, }, export_manifest: { assets: { setting: "settings.json" }}, @@ -101,6 +103,14 @@ describe("lib/data/ModPack", function() { default_value: "a string", }, }, + "color-setting": { + "color": { + name: "color", + type: "color-setting", + setting_type: "runtime-per-user", + default_value: { "r": 1, "g": 1, "b": 1, "a": 1 }, + }, + }, }; const mockLogger = { warn: () => {} }; it("should fill in defaults for settings", function() { @@ -115,6 +125,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string": { "value": "a string" }, + "color": { "value": { "r": 1, "g": 1, "b": 1, "a": 1 } }, }, }); }); @@ -128,6 +139,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string": { "value": "spam" }, + "color": { "value": { "r": 0, "g": 0.5, "b": 1, "a": 1 } }, }, }}); pack.fillDefaultSettings(prototypes, mockLogger); @@ -140,6 +152,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string": { "value": "spam" }, + "color": { "value": { "r": 0, "g": 0.5, "b": 1, "a": 1 } }, }, }); }); @@ -204,6 +217,7 @@ describe("lib/data/ModPack", function() { }, "runtime-per-user": { "string-setting": { "value": "a string" }, + "color-setting": { "value": { "r": 1, "g": 0.5, "b": 0, "a": 1 } }, }, }}); @@ -248,7 +262,7 @@ describe("lib/data/ModPack", function() { ...istr("runtime-per-user"), // name Uint8Array.from([5, 0]), // dictionary - new Uint8Array(Uint32Array.from([1]).buffer), // items + new Uint8Array(Uint32Array.from([2]).buffer), // items ...istr("string-setting"), // name Uint8Array.from([5, 0]), // dictinary @@ -257,6 +271,30 @@ describe("lib/data/ModPack", function() { ...istr("value"), // name Uint8Array.from([3, 0, 0, "a string".length]), // string Buffer.from("a string"), + + ...istr("color-setting"), // name + Uint8Array.from([5, 0]), // dictinary + new Uint8Array(Uint32Array.from([1]).buffer), // items + + ...istr("value"), // name + Uint8Array.from([5, 0]), // dictinary + new Uint8Array(Uint32Array.from([4]).buffer), // items + + ...istr("r"), // name + Uint8Array.from([2, 0]), // number + new Uint8Array(Float64Array.from([1]).buffer), + + ...istr("g"), // name + Uint8Array.from([2, 0]), // number + new Uint8Array(Float64Array.from([0.5]).buffer), + + ...istr("b"), // name + Uint8Array.from([2, 0]), // number + new Uint8Array(Float64Array.from([0]).buffer), + + ...istr("a"), // name + Uint8Array.from([2, 0]), // number + new Uint8Array(Float64Array.from([1]).buffer), ]) ); /* eslint-enable indent */