From 3f6a7e43361a4ea89ddc381397cf909c7c7bab68 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 2 Feb 2023 00:00:54 +0100 Subject: [PATCH] Format mod settings with prototypes and locale in web UI Use exported settings prototypes and locale to format the settings UI to with mod names, settings names, and select dropdowns. This is a crude approximation of how the game presents these settings to the user. --- .../web_ui/src/components/ModPackViewPage.jsx | 166 ++++++++++++++---- 1 file changed, 134 insertions(+), 32 deletions(-) diff --git a/packages/web_ui/src/components/ModPackViewPage.jsx b/packages/web_ui/src/components/ModPackViewPage.jsx index 0d2a426f..69393489 100644 --- a/packages/web_ui/src/components/ModPackViewPage.jsx +++ b/packages/web_ui/src/components/ModPackViewPage.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useContext, useRef, useState } from "react"; +import React, { Fragment, useEffect, useContext, useRef, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, PageHeader, Pagination, @@ -353,49 +353,115 @@ function pickValue(map) { return [...map].map(([name, value]) => [name, value.value]); } -function SettingsTable(props) { - function controls(scope, fields) { - return fields.map(([fieldName, fieldValue]) => { - const isBoolean = typeof fieldValue === "boolean"; - const isNumber = typeof fieldValue === "number"; +function groupToMap(array, fn) { + const map = new Map(); + for (let index = 0; index < array.length; index++) { + let key = fn(array[index], index, array); + if (!map.has(key)) { + map.set(key, []); + } + map.get(key).push(array[index]); + } + return map; +} - return `${mod.name}_${mod.version}`)); + const modTitles = new Map(modList + .filter(mod => modsInPack.has(`${mod.name}_${mod.version}`)) + .map(mod => [`${mod.name}`, mod.title]) + ); + + const types = ["bool-setting", "int-setting", "double-setting", "string-setting"]; + let prototypes = Object.entries(props.prototypes) + .filter(([type, _]) => types.includes(type)) + .flatMap(([_, settingPrototypes]) => Object.values(settingPrototypes)) + ; + + function controls(scope, storedFields) { + let fields = new Map(prototypes + .filter(p => p.setting_type === scope) + .sort((a, b) => strcmp(a.mod, b.mod) || strcmp(a.order || "", b.order || "") || strcmp(a.name, b.name)) + .map(p => [p.name, p]) + ); + for (let [name, value] of storedFields) { + if (!fields.has(name)) { + let type; + if (typeof value === "boolean") { + type = "bool-setting"; + } else if (typeof value === "number") { + type = "double-setting"; + } else if (typeof value === "string") { + type = "string-setting"; + } else { + throw Error(`Unhandled setting type ${typeof value}`); } - > - {isBoolean ? : } - ; - }); + fields.set(name, { mod: "Unknown Fields", name, setting_type: scope, type }); + } + } + let fieldsByMod = groupToMap([...fields.values()], prototype => prototype.mod); + return [...fieldsByMod].map(([mod, fieldsForMod]) => + + {props.locale.get(`mod-name.${mod}`) || modTitles.get(mod) || mod} + + {[...fieldsForMod].map(prototype => { + const name = prototype.name; + const isBoolean = prototype.type === "bool-setting"; + const isNumber = ["int-setting", "double-setting"].includes(prototype.type); + const isSelect = Boolean(prototype.allowed_values); + let input; + if (isBoolean) { + input = ; + } else if (isSelect) { + const options = Object.values(prototype.allowed_values).map(value => ({ + value, + label: props.locale.get(`string-mod-setting.${name}-${value}`) || value, + })); + input = ; + } + + return + {input} + ; + })} + ); } return <> - Use clusterioctl to add and remove fields. - Startup + Run export data on an instance to update available fields. + Startup {controls("startup", pickValue(props.modPack.settings["startup"]))} - Map + Map These only apply for new saves. {controls("runtime-global", pickValue(props.modPack.settings["runtime-global"]))} - Per Player + Per Player Default settings for new players, these only apply for new saves. {controls("runtime-per-user", pickValue(props.modPack.settings["runtime-per-user"]))} ; @@ -457,6 +523,33 @@ function ExportButton(props) { ; } +function useExportedAsset(modPack, asset) { + let [assetData, setAssetData] = useState(asset === "locale" ? new Map() : {}); + let assetFilename; + if (modPack instanceof libData.ModPack && modPack.exportManifest && modPack.exportManifest.assets[asset]) { + assetFilename = modPack.exportManifest.assets[asset]; + } + useEffect(() => { + async function load() { + if (!assetFilename) { + return; + } + + let response = await fetch(`${staticRoot}static/${assetFilename}`); + if (response.ok) { + let data = await response.json(); + setAssetData(asset === "locale" ? new Map(data) : data); + } else { + logger.error(`Error loading mod pack asset "${asset}" (${response.status}): ${await response.text()}`); + } + } + + load().catch(notifyErrorHandler(`Loading mod pack asset "${asset}" failed`)); + }, [assetFilename]); + + return assetData; +} + export default function ModPackViewPage() { let account = useAccount(); let history = useHistory(); @@ -466,6 +559,8 @@ export default function ModPackViewPage() { const [form] = Form.useForm(); let [modPack] = useModPack(modPackId); + let prototypes = useExportedAsset(modPack, "settings"); + let locale = useExportedAsset(modPack, "locale"); let [changes, setChanges] = useState([]); let [modifiedModPack, setModifiedModPack] = useState(modPack); let syncTimeout = useRef(); @@ -657,7 +752,14 @@ export default function ModPackViewPage() { - +