From 25955c461ef40161f8140a9009eeb56371ddf177 Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:56:19 +0900 Subject: [PATCH] Dividing Setting string into Visual and self --- GUI/electron/src/modules/generator.js | 48 +++++++- GUI/electron/src/preload.ts | 44 ++++++- .../app/@theme/styles/theme.ootr-dark.scss | 2 +- .../app/@theme/styles/theme.ootr-default.scss | 2 +- .../pages/generator/generator.component.html | 16 +++ .../pages/generator/generator.component.scss | 6 +- .../pages/generator/generator.component.ts | 112 ++++++++++++++++- GUI/src/app/providers/GUIGlobal.ts | 83 ++++++++++++- Plandomizer.py | 5 + SettingTypes.py | 71 +++++------ Settings.py | 116 +++++++++++++++--- SettingsList.py | 39 ++++-- SettingsToJson.py | 3 + 13 files changed, 468 insertions(+), 79 deletions(-) diff --git a/GUI/electron/src/modules/generator.js b/GUI/electron/src/modules/generator.js index da63e408a1..fde4db831b 100644 --- a/GUI/electron/src/modules/generator.js +++ b/GUI/electron/src/modules/generator.js @@ -14,6 +14,12 @@ function promiseFromChildProcess(child) { }); } +function _extractStringToken(output) { + // allow URL-safe base64 chars as well (-, _) + let m = output.match(/[A-Za-z0-9_-]+/g); + return (m && m[0]) ? m[0] : ""; +} + function isMulti(args) { if (("world_count" in args) && args["world_count"] > 1) @@ -337,7 +343,7 @@ function parseSettings(pythonPath, randoPath) { reject(output); } else { - resolve(output.match(/([a-zA-Z0-9])\w+/g)[0]); + resolve(_extractStringToken(output)); } }).catch(err => { @@ -347,6 +353,44 @@ function parseSettings(pythonPath, randoPath) { }); } +function getVisualSettings(pythonPath, randoPath, visualSettingsString) { + return new Promise(function (resolve, reject) { + let output = ""; + let error = false; + let args = ['--convert_visual_settings', '--visual_settings_string', visualSettingsString]; + let settingsPY = spawn(pythonPath + ' ' + '"' + randoPath + '"', args, { shell: true }) + .on('error', err => { + console.error("[getVisualSettings] Error spawning process:", err); + reject(err); + }); + settingsPY.stdout.on('data', data => { output += data.toString(); error = false; }); + settingsPY.stderr.on('data', data => { output += data.toString(); error = true; }); + promiseFromChildProcess(settingsPY).then(function () { + if (error) reject(output); + else resolve(output.replace(/\r?\n|\r/g, "\r\n")); + }).catch(err => reject(err)); + }); +} + +function parseVisualSettings(pythonPath, randoPath) { + return new Promise(function (resolve, reject) { + let output = ""; + let error = false; + let args = ['--convert_visual_settings']; + let settingsPY = spawn(pythonPath + ' ' + '"' + randoPath + '"', args, { shell: true }) + .on('error', err => { + console.error("[parseVisualSettings] Error spawning process:", err); + reject(err); + }); + settingsPY.stdout.on('data', data => { output += data.toString(); error = false; }); + settingsPY.stderr.on('data', data => { output += data.toString(); error = true; }); + promiseFromChildProcess(settingsPY).then(() => { + if (error) reject(output); + else resolve(_extractStringToken(output)); + }).catch(err => reject(err)); + }); +} + function getUpdatedDynamicSetting(pythonPath, scriptPath, settingName) { return new Promise(function (resolve, reject) { @@ -394,6 +438,8 @@ module.exports = new EventEmitter(); module.exports.getSettings = getSettings; module.exports.parseSettings = parseSettings; +module.exports.getVisualSettings = getVisualSettings; +module.exports.parseVisualSettings = parseVisualSettings; module.exports.romBuilding = romBuilding; module.exports.cancelRomBuilding = cancelRomBuilding; module.exports.testPythonPath = testPythonPath; diff --git a/GUI/electron/src/preload.ts b/GUI/electron/src/preload.ts index 55c48b30a3..156921de06 100644 --- a/GUI/electron/src/preload.ts +++ b/GUI/electron/src/preload.ts @@ -62,6 +62,15 @@ function dumpSettingsToFile(settingsObj) { fs.writeFileSync(pythonSourcePath + "settings.sav", JSON.stringify(settingsObj, null, 4)); } +function dumpSettingsToFileForPython(settingsObj) { + // Avoid feeding UI-only string fields into python Settings + let obj = Object.assign({}, settingsObj); + delete obj["settings_string"]; + delete obj["visual_settings_string"]; + obj["check_version"] = true; + fs.writeFileSync(pythonSourcePath + "settings.sav", JSON.stringify(obj, null, 4)); +} + function dumpPresetsToFile(presetsString: string) { fs.writeFileSync(pythonSourcePath + "presets.sav", presetsString); } @@ -239,7 +248,7 @@ post.on('convertSettingsToString', function (event) { return false; //Write settings obj to settings.sav - dumpSettingsToFile(data); + dumpSettingsToFileForPython(data); //console.log("generate string with settings obj", data); @@ -259,6 +268,21 @@ post.on('convertSettingsToString', function (event) { return true; }); +post.on('convertVisualSettingsToString', function (event) { + let data = event.data; + if (!data || typeof (data) != "object" || Object.keys(data).length < 1) return false; + dumpSettingsToFileForPython(data); + generator.parseVisualSettings(pythonPath, pythonGeneratorPath).then(res => { + post.send(window, 'convertVisualSettingsToStringSuccess', res); + }).catch((err) => { + if (typeof err === "string" && err.includes("ImportError: No module named tkinter")) { + displayPythonErrorAndExit(true); + } + post.send(window, 'convertVisualSettingsToStringError', err); + }); + return true; +}); + post.on('updateDynamicSetting', function (event) { let data = event.data; @@ -305,6 +329,20 @@ post.on('convertStringToSettings', function (event) { return true; }); +post.on('convertStringToVisualSettings', function (event) { + let data = event.data; + if (!data || typeof (data) != "string" || data.length < 1) return false; + generator.getVisualSettings(pythonPath, pythonGeneratorPath, data).then(res => { + post.send(window, 'convertStringToVisualSettingsSuccess', res); + }).catch((err) => { + if (typeof err === "string" && err.includes("ImportError: No module named tkinter")) { + displayPythonErrorAndExit(true); + } + post.send(window, 'convertStringToVisualSettingsError', err); + }); + return true; +}); + post.on('saveCurrentPresetsToFile', function (event) { let data = event.data; @@ -331,8 +369,8 @@ post.on('generateSeed', function (event) { return false; //Write settings obj to settings.sav - dumpSettingsToFile(settingsFile); - + dumpSettingsToFileForPython(settingsFile); + //console.log("generate seed with settings:", data); generator.romBuilding(pythonPath, pythonGeneratorPath, data).then(res => { diff --git a/GUI/src/app/@theme/styles/theme.ootr-dark.scss b/GUI/src/app/@theme/styles/theme.ootr-dark.scss index 200a6eda6f..d29aa1bcb2 100644 --- a/GUI/src/app/@theme/styles/theme.ootr-dark.scss +++ b/GUI/src/app/@theme/styles/theme.ootr-dark.scss @@ -128,7 +128,7 @@ $nb-themes: nb-register-theme( header-text-font-size: 13.125px, header-text-line-height: 17.5px, - footer-height: 203px, + footer-height: 253px, footer-text-font-size: 13.125px, footer-text-line-height: 17.5px, diff --git a/GUI/src/app/@theme/styles/theme.ootr-default.scss b/GUI/src/app/@theme/styles/theme.ootr-default.scss index fd5816d154..c225b4c83c 100644 --- a/GUI/src/app/@theme/styles/theme.ootr-default.scss +++ b/GUI/src/app/@theme/styles/theme.ootr-default.scss @@ -128,7 +128,7 @@ $nb-themes: nb-register-theme( header-text-font-size: 13.125px, header-text-line-height: 17.5px, - footer-height: 215px, + footer-height: 265px, footer-text-font-size: 13.125px, footer-text-line-height: 17.5px, diff --git a/GUI/src/app/pages/generator/generator.component.html b/GUI/src/app/pages/generator/generator.component.html index 11d2123557..54396433f1 100644 --- a/GUI/src/app/pages/generator/generator.component.html +++ b/GUI/src/app/pages/generator/generator.component.html @@ -271,6 +271,12 @@

{{section.text}}

+

Visual Settings String

+ + + + +

Seed

@@ -285,6 +291,16 @@

{{section.text}}

+

Visual Settings String

+ + + + + +

Seed

diff --git a/GUI/src/app/pages/generator/generator.component.scss b/GUI/src/app/pages/generator/generator.component.scss index 05dfe76d7e..61e0eba6af 100644 --- a/GUI/src/app/pages/generator/generator.component.scss +++ b/GUI/src/app/pages/generator/generator.component.scss @@ -65,12 +65,12 @@ } .settingsTabset { - height: calc(91vh - 260px); + height: calc(91vh - 310px); min-height: 490px; padding: 0 7px !important; @media (max-width: 400px) { - height: calc(88vh - 260px); + height: calc(88vh - 310px); min-height: 460px; } @@ -89,7 +89,7 @@ margin-top: 5px; @media (max-width: 700px) { - height: 210px; + height: 310px; } nb-tab { diff --git a/GUI/src/app/pages/generator/generator.component.ts b/GUI/src/app/pages/generator/generator.component.ts index 00814d2750..745a040ac2 100644 --- a/GUI/src/app/pages/generator/generator.component.ts +++ b/GUI/src/app/pages/generator/generator.component.ts @@ -101,7 +101,7 @@ export class GeneratorComponent implements OnInit { //Electron only: Ensure settings string is up-to-date on app launch if (this.global.getGlobalVar('electronAvailable')) - this.getSettingsString(); + this.getSettingsStrings(); else //Web only: Check if we should auto import settings/presets from a prior version this.checkAutoImportSettings(); } @@ -432,10 +432,57 @@ export class GeneratorComponent implements OnInit { }); } + private normalizeSettingsString(value: any): string { + if (typeof value !== "string") return ""; + let s = value.trim().replace(/[^a-zA-Z0-9_-]/g, ""); + if (s.length > 0 && s.length < 8) s = s.padEnd(8, "A"); + return s; + } + copySettingsString() { this.global.copyToClipboard(this.global.generator_settingsMap["settings_string"]); } + + copyVisualSettingsString() { + this.global.copyToClipboard(this.global.generator_settingsMap["visual_settings_string"]); + } + + getSettingsStrings() { + this.settingsLocked = true; + this.global.convertSettingsToString() + .then(settingsStr => { + this.global.generator_settingsMap["settings_string"] = settingsStr; + return this.global.convertVisualSettingsToString(); + }) + .then(visualStr => { + this.global.generator_settingsMap["visual_settings_string"] = this.normalizeSettingsString(visualStr); + this.global.saveCurrentSettingsToFile(); + this.settingsLocked = false; + if (this.settingsBusy) { + this.settingsBusy = false; + this.afterSettingChange(this.settingsBusySaveOnly); + this.settingsBusySaveOnly = true; + } + this.cd.markForCheck(); + this.cd.detectChanges(); + }) + .catch(err => { + this.settingsLocked = false; + if (this.settingsBusy) { + this.settingsBusy = false; + this.afterSettingChange(this.settingsBusySaveOnly); + this.settingsBusySaveOnly = true; + } + this.cd.markForCheck(); + this.cd.detectChanges(); + this.dialogService.open(ErrorDetailsWindowComponent, { + autoFocus: true, closeOnBackdropClick: true, closeOnEsc: true, + hasBackdrop: true, hasScroll: false, context: { errorMessage: err } + }); + }); + } + getSettingsString() { this.settingsLocked = true; @@ -476,12 +523,43 @@ export class GeneratorComponent implements OnInit { }); } + + getVisualSettingsString() { + this.settingsLocked = true; + this.global.convertVisualSettingsToString().then(res => { + this.global.generator_settingsMap["visual_settings_string"] = this.normalizeSettingsString(res); + this.global.saveCurrentSettingsToFile(); + this.settingsLocked = false; + if (this.settingsBusy) { + this.settingsBusy = false; + this.afterSettingChange(this.settingsBusySaveOnly); + this.settingsBusySaveOnly = true; + } + this.cd.markForCheck(); + this.cd.detectChanges(); + }).catch((err) => { + this.settingsLocked = false; + if (this.settingsBusy) { + this.settingsBusy = false; + this.afterSettingChange(this.settingsBusySaveOnly); + this.settingsBusySaveOnly = true; + } + this.cd.markForCheck(); + this.cd.detectChanges(); + this.dialogService.open(ErrorDetailsWindowComponent, { + autoFocus: true, closeOnBackdropClick: true, closeOnEsc: true, + hasBackdrop: true, hasScroll: false, context: { errorMessage: err } + }); + }); + } + importSettingsString() { this.generatorBusy = true; - this.global.convertStringToSettings(this.global.generator_settingsMap["settings_string"]).then(res => { - + const normalized = this.normalizeSettingsString(this.global.generator_settingsMap["settings_string"]); + this.global.generator_settingsMap["settings_string"] = normalized; + this.global.convertStringToSettings(normalized).then(res => { //console.log(res); this.global.applySettingsObject(res); @@ -506,6 +584,30 @@ export class GeneratorComponent implements OnInit { }); } + + importVisualSettingsString() { + this.generatorBusy = true; + const normalized = this.normalizeSettingsString(this.global.generator_settingsMap["visual_settings_string"]); + this.global.generator_settingsMap["visual_settings_string"] = normalized; + this.global.convertStringToVisualSettings(normalized).then(res => { + this.global.applySettingsObject(res); + this.global.saveCurrentSettingsToFile(); + this.recheckAllSettings("", false, true); + this.generatorBusy = false; + this.cd.markForCheck(); + this.cd.detectChanges(); + }).catch((_err) => { + this.generatorBusy = false; + this.cd.markForCheck(); + this.cd.detectChanges(); + this.dialogService.open(DialogWindowComponent, { + autoFocus: true, closeOnBackdropClick: true, closeOnEsc: true, + hasBackdrop: true, hasScroll: false, + context: { dialogHeader: "Error", dialogMessage: "The entered visual settings string seems to be invalid!" } + }); + }); + } + getPresetArray() { if (typeof (this.global.generator_presets) == "object") return Object.keys(this.global.generator_presets); @@ -1463,7 +1565,7 @@ export class GeneratorComponent implements OnInit { this.cd.detectChanges(); } else { - this.getSettingsString(); + this.getSettingsStrings(); } }, 0); } @@ -1563,4 +1665,4 @@ export class GeneratorComponent implements OnInit { } } } -} +} \ No newline at end of file diff --git a/GUI/src/app/providers/GUIGlobal.ts b/GUI/src/app/providers/GUIGlobal.ts index a6c82ca209..11fed03077 100644 --- a/GUI/src/app/providers/GUIGlobal.ts +++ b/GUI/src/app/providers/GUIGlobal.ts @@ -615,8 +615,10 @@ export class GUIGlobal implements OnDestroy { //Add GUI only options this.generator_settingsMap["settings_string"] = userSettings && "settings_string" in userSettings ? userSettings["settings_string"] : ""; + this.generator_settingsMap["visual_settings_string"] = userSettings && "visual_settings_string" in userSettings ? userSettings["visual_settings_string"] : ""; this.generator_settingsMap["theme"] = userSettings && "theme" in userSettings ? userSettings["theme"] : ""; this.generator_settingsVisibilityMap["settings_string"] = true; + this.generator_settingsVisibilityMap["visual_settings_string"] = true; console.log("JSON Settings Data:", guiSettings); console.log("Last User Settings:", userSettings); @@ -1121,6 +1123,7 @@ export class GUIGlobal implements OnDestroy { //Not mapped settings need to be deleted manually delete settingsFile["settings_string"]; + delete settingsFile["visual_settings_string"]; delete settingsFile["theme"] //Delete all shared = false keys from map since they aren't included in the seed @@ -1200,6 +1203,40 @@ export class GUIGlobal implements OnDestroy { }); } + + convertVisualSettingsToString() { + var self = this; + return new Promise(function (resolve, reject) { + if (self.getGlobalVar('electronAvailable')) { + //Electron + post.send(window, 'convertVisualSettingsToString', self.createSettingsFileObject(true, false, false, true)).then(_event => { + var listenerSuccess = post.once('convertVisualSettingsToStringSuccess', function (event) { + listenerError.cancel(); + resolve(event.data); + }); + var listenerError = post.once('convertVisualSettingsToStringError', function (event) { + listenerSuccess.cancel(); + console.error("[convertVisualSettingsToString] Python Error:", event.data); + reject(event.data); + }); + }).catch(err => { + console.error("[convertVisualSettingsToString] Post-Robot Error:", err); + reject(err); + }); + } else { + //Web + let url = (window).location.protocol + "//" + (window).location.host + + "/settings/parse?version=" + self.getGlobalVar("webSourceVersion") + "&visual=1"; + self.http.post(url, JSON.stringify(self.createSettingsFileObject(false, false, true)), { + responseType: "text", headers: { "Content-Type": "application/json" } + }).toPromise().then(res => resolve(res)).catch(err => { + console.error("[convertVisualSettingsToString] Web Error:", err); + reject(err); + }); + } + }); + } + convertStringToSettings(settingsString: string) { var self = this; @@ -1246,6 +1283,38 @@ export class GUIGlobal implements OnDestroy { }); } + convertStringToVisualSettings(visualSettingsString: string) { + var self = this; + return new Promise(function (resolve, reject) { + if (self.getGlobalVar('electronAvailable')) { //Electron + post.send(window, 'convertStringToVisualSettings', visualSettingsString).then(_event => { + var listenerSuccess = post.once('convertStringToVisualSettingsSuccess', function (event) { + listenerError.cancel(); + let data = JSON.parse(event.data); + resolve(data); + }); + var listenerError = post.once('convertStringToVisualSettingsError', function (event) { + listenerSuccess.cancel(); + console.error("[convertStringToVisualSettings] Python Error:", event.data); + reject(event.data); + }); + }).catch(err => { + console.error("[convertStringToVisualSettings] Post-Robot Error:", err); + reject(err); + }); + } else { //Web + let url = (window).location.protocol + "//" + (window).location.host + + "/settings/get?version=" + self.getGlobalVar("webSourceVersion") + + "&settingsString=" + encodeURIComponent(visualSettingsString) + + "&visual=1"; + self.http.get(url, { responseType: "json" }).toPromise().then(res => resolve(res)).catch(err => { + console.error("[convertStringToVisualSettings] Web Error:", err); + reject(err); + }); + } + }); + } + createPresetFileObject() { let presetsFile: any = {}; @@ -1293,6 +1362,10 @@ export class GUIGlobal implements OnDestroy { return; } + // UI-only strings: never send to python + delete settingsMap["settings_string"]; + delete settingsMap["visual_settings_string"]; + //Hack: fromPatchFile forces generation count to 1 to avoid wrong percentage calculation if (fromPatchFile) settingsMap["count"] = 1; @@ -1559,6 +1632,10 @@ export class GUIGlobal implements OnDestroy { throw { error: "The generation was aborted due to previous errors!" }; } + // UI-only strings: never send to python + delete settingsFile["settings_string"]; + delete settingsFile["visual_settings_string"]; + //Add distribution file back into map as string if available, else clear it if (plandoFileSeed) { settingsFile["distribution_file"] = plandoFileSeed; @@ -1661,6 +1738,10 @@ export class GUIGlobal implements OnDestroy { throw { error: "The patching was aborted due to previous errors!" }; } + // UI-only strings: never send to python + delete settingsFile["settings_string"]; + delete settingsFile["visual_settings_string"]; + //Add cosmetics plando file back into map as string if available, else clear it if (plandoFileCosmetics) { settingsFile["cosmetic_file"] = plandoFileCosmetics; @@ -1681,4 +1762,4 @@ export class GUIGlobal implements OnDestroy { throw { error: "Patcher not available!" }; } } -} +} \ No newline at end of file diff --git a/Plandomizer.py b/Plandomizer.py index 9e299117d6..c9bc37fef6 100644 --- a/Plandomizer.py +++ b/Plandomizer.py @@ -53,6 +53,10 @@ class InvalidFileException(Exception): ) +def normalize_settings_string(s: str): + return s + "A" * (8 - len(s)) + + class Record: def __init__(self, properties: Optional[dict[str, Any]] = None, src_dict: Optional[dict[str, Any]] = None) -> None: self.properties: dict[str, Any] = properties if properties is not None else getattr(self, "properties") @@ -1302,6 +1306,7 @@ def to_json(self, include_output: bool = True, spoiler: bool = True) -> dict[str 'password': CollapseList(self.password), ':seed': self.settings.seed, ':settings_string': self.settings.settings_string, + ':visual_settings_string': normalize_settings_string(self.settings.visual_settings_string), ':enable_distribution_file': self.settings.enable_distribution_file, 'settings': self.settings.to_json(), } diff --git a/SettingTypes.py b/SettingTypes.py index 9a6ac734b2..d23bf184f1 100644 --- a/SettingTypes.py +++ b/SettingTypes.py @@ -11,9 +11,10 @@ class SettingInfo: def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Optional[str], shared: bool, choices: Optional[dict | list] = None, default: Any = None, disabled_default: Any = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, - cosmetic: bool = False) -> None: + cosmetic: bool = False, visual_shared: bool = False) -> None: self.type: type = setting_type # type of the setting's value, used to properly convert types to setting strings self.shared: bool = shared # whether the setting is one that should be shared, used in converting settings to a string + self.visual_shared: bool = visual_shared # whether the setting is one that should be shared, but not effecting the seed itself self.cosmetic: bool = cosmetic # whether the setting should be included in the cosmetic log self.gui_text: Optional[str] = gui_text self.gui_type: Optional[str] = gui_type @@ -33,7 +34,7 @@ def __init__(self, setting_type: type, gui_text: Optional[str], gui_type: Option self.reverse_choices: dict = {v: k for k, v in self.choices.items()} # number of bits needed to store the setting, used in converting settings to a string - if shared: + if self.shared or self.visual_shared: if self.gui_params.get('min') and self.gui_params.get('max') and not choices: self.bitwidth = math.ceil(math.log(self.gui_params.get('max') - self.gui_params.get('min') + 1, 2)) else: @@ -103,7 +104,7 @@ def __init__(self, gui_text: Optional[str], gui_type: Optional[str], gui_tooltip gui_params: Optional[dict] = None) -> None: super().__init__(setting_type=type(None), gui_text=gui_text, gui_type=gui_type, shared=False, choices=None, default=None, disabled_default=None, disable=None, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=False) + gui_params=gui_params, cosmetic=False, visual_shared=False) def __get__(self, obj, obj_type=None) -> None: raise Exception(f"{self.name} is not a setting and cannot be retrieved.") @@ -115,7 +116,7 @@ def __set__(self, obj, value: str) -> None: class SettingInfoBool(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, default: Optional[bool] = None, disabled_default: Optional[bool] = None, disable: Optional[dict] = None, gui_tooltip: Optional[str] = None, - gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: choices = { True: 'checked', False: 'unchecked', @@ -123,7 +124,7 @@ def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: boo super().__init__(setting_type=bool, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) def __get__(self, obj, obj_type=None) -> bool: value = super().__get__(obj, obj_type) @@ -141,10 +142,10 @@ class SettingInfoStr(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool = False, choices: Optional[dict | list] = None, default: Optional[str] = None, disabled_default: Optional[str] = None, disable: Optional[dict] = None, - gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(setting_type=str, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) def __get__(self, obj, obj_type=None) -> str: value = super().__get__(obj, obj_type) @@ -162,10 +163,10 @@ class SettingInfoInt(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, choices: Optional[dict | list] = None, default: Optional[int] = None, disabled_default: Optional[int] = None, disable: Optional[dict] = None, - gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(setting_type=int, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) def __get__(self, obj, obj_type=None) -> int: value = super().__get__(obj, obj_type) @@ -183,10 +184,10 @@ class SettingInfoList(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, choices: Optional[dict | list] = None, default: Optional[list] = None, disabled_default: Optional[list] = None, disable: Optional[dict] = None, - gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(setting_type=list, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) def __get__(self, obj, obj_type=None) -> list: value = super().__get__(obj, obj_type) @@ -204,10 +205,10 @@ class SettingInfoDict(SettingInfo): def __init__(self, gui_text: Optional[str], gui_type: Optional[str], shared: bool, choices: Optional[dict | list] = None, default: Optional[dict] = None, disabled_default: Optional[dict] = None, disable: Optional[dict] = None, - gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + gui_tooltip: Optional[str] = None, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(setting_type=dict, gui_text=gui_text, gui_type=gui_type, shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) def __get__(self, obj, obj_type=None) -> dict: value = super().__get__(obj, obj_type) @@ -236,70 +237,70 @@ def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None, class Checkbutton(SettingInfoBool): def __init__(self, gui_text: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[bool] = None, default: bool = False, shared: bool = False, - gui_params: Optional[dict] = None, cosmetic: bool = False): + gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False): super().__init__(gui_text=gui_text, gui_type='Checkbutton', shared=shared, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Combobox(SettingInfoStr): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Radiobutton(SettingInfoStr): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[str], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Radiobutton', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Fileinput(SettingInfoStr): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Fileinput', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Directoryinput(SettingInfoStr): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Directoryinput', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Textinput(SettingInfoStr): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list] = None, default: Optional[str] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[str] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Textinput', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class ComboboxInt(SettingInfoInt): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[int], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='Combobox', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Scale(SettingInfoInt): def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: int, maximum: int, step: int = 1, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: choices = { i: str(i) for i in range(minimum, maximum+1, step) } @@ -312,14 +313,14 @@ def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: int super().__init__(gui_text=gui_text, gui_type='Scale', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class Numberinput(SettingInfoInt): def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: Optional[int] = None, maximum: Optional[int] = None, gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[int] = None, shared: bool = False, gui_params: Optional[dict] = None, - cosmetic: bool = False) -> None: + cosmetic: bool = False, visual_shared: bool = False) -> None: if gui_params is None: gui_params = {} if minimum is not None: @@ -329,22 +330,22 @@ def __init__(self, gui_text: Optional[str], default: Optional[int], minimum: Opt super().__init__(gui_text=gui_text, gui_type='Numberinput', shared=shared, choices=None, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class MultipleSelect(SettingInfoList): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='MultipleSelect', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) class SearchBox(SettingInfoList): def __init__(self, gui_text: Optional[str], choices: Optional[dict | list], default: Optional[list], gui_tooltip: Optional[str] = None, disable: Optional[dict] = None, disabled_default: Optional[list] = None, - shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False) -> None: + shared: bool = False, gui_params: Optional[dict] = None, cosmetic: bool = False, visual_shared: bool = False) -> None: super().__init__(gui_text=gui_text, gui_type='SearchBox', shared=shared, choices=choices, default=default, disabled_default=disabled_default, disable=disable, gui_tooltip=gui_tooltip, - gui_params=gui_params, cosmetic=cosmetic) + gui_params=gui_params, cosmetic=cosmetic, visual_shared=visual_shared) diff --git a/Settings.py b/Settings.py index 1753d9c91d..2f4f20e67a 100644 --- a/Settings.py +++ b/Settings.py @@ -70,6 +70,18 @@ def get_preset_files() -> list[str]: if fn.endswith('.json')) +def _jsonify_starting_item_record(record): + if isinstance(record, int): + return record + if isinstance(record, dict): + return {k: _jsonify_starting_item_record(v) for k, v in record.items()} + if hasattr(record, "count") and isinstance(record.count, int): + return record.count + if hasattr(record, "to_json"): + return record.to_json() + return record + + # holds the particular choices for a run's settings class Settings(SettingInfos): # add the settings as fields, and calculate information based on them @@ -96,7 +108,10 @@ def __init__(self, settings_dict: dict[str, Any], strict: bool = False) -> None: self.world_count = 255 self._disabled: set[str] = set() - self.settings_string: str = self.get_settings_string() + + self.visual_settings_string: str = self.get_visual_settings_string() + + self.settings_string: str = self.get_gameplay_settings_string() self.distribution: Distribution = Distribution(self) self.update_seed(self.seed) self.custom_seed: bool = False @@ -107,12 +122,29 @@ def copy(self) -> Settings: return settings def update(self, settings_dict: dict[str, Any], *, initialize: bool = False) -> None: + touched_gameplay = False + touched_visual = False + for info in self.setting_infos.values(): if info.type is type(None): continue if not initialize and info.name not in settings_dict: continue - setattr(self, info.name, settings_dict[info.name] if info.name in settings_dict else info.default) + + val = settings_dict[info.name] if info.name in settings_dict else info.default + setattr(self, info.name, val) + + if info.bitwidth > 0: + if getattr(info, "shared", False): + touched_gameplay = True + if getattr(info, "visual_shared", False) or getattr(info, "cosmetic", False): + touched_visual = True + + if not initialize and (touched_gameplay or touched_visual): + self._refresh_settings_strings() + + if touched_gameplay: + self.numeric_seed = self.get_numeric_seed() def get_settings_display(self) -> str: padding = 0 @@ -129,12 +161,22 @@ def get_settings_display(self) -> str: output += name + val + '\n' return output - def get_settings_string(self) -> str: + def _refresh_settings_strings(self) -> None: + self.visual_settings_string = self.get_visual_settings_string() + self.settings_string = self.get_gameplay_settings_string() # backward compat + + def get_gameplay_settings_string(self) -> str: + return self._encode_settings_string(lambda s: s.shared and s.bitwidth > 0) + + def get_visual_settings_string(self) -> str: + return self._encode_settings_string(lambda s: s.visual_shared and s.bitwidth > 0) + + def _encode_settings_string(self, pred) -> str: bits = [] - for setting in filter(lambda s: s.shared and s.bitwidth > 0, self.setting_infos.values()): + for setting in filter(pred, self.setting_infos.values()): value = getattr(self, setting.name) i_bits = [] - if setting.name in LEGACY_STARTING_ITEM_SETTINGS: + if setting.shared and setting.name in LEGACY_STARTING_ITEM_SETTINGS: items = LEGACY_STARTING_ITEM_SETTINGS[setting.name] value = [] for entry in items.values(): @@ -192,10 +234,31 @@ def get_settings_string(self) -> str: bits += i_bits return bit_string_to_text(bits) + def get_settings_string(self) -> str: + return self.get_gameplay_settings_string() + def update_with_settings_string(self, text: str) -> None: + self.update_with_gameplay_settings_string(text) + + def update_with_gameplay_settings_string(self, text: str) -> None: + self._decode_settings_string(text, lambda s: s.shared and s.bitwidth > 0) + self._refresh_settings_strings() + self.numeric_seed = self.get_numeric_seed() + + def update_with_visual_settings_string(self, text: str) -> None: + self._decode_settings_string(text, lambda s: s.visual_shared and s.bitwidth > 0) + self._refresh_settings_strings() + self.numeric_seed = self.get_numeric_seed() + + def _decode_settings_string(self, text: str, pred) -> None: bits = text_to_bit_string(text) - for setting in filter(lambda s: s.shared and s.bitwidth > 0, self.setting_infos.values()): + targets = [s for s in self.setting_infos.values() if pred(s)] + need = sum(s.bitwidth for s in targets) + if len(bits) < need: + bits += [0] * (need - len(bits)) + + for setting in targets: cur_bits = bits[:setting.bitwidth] bits = bits[setting.bitwidth:] value = None @@ -205,6 +268,11 @@ def update_with_settings_string(self, text: str) -> None: index = 0 for b in range(setting.bitwidth): index |= cur_bits[b] << b + if index >= len(setting.choice_list): + try: + index = setting.choice_list.index(setting.default) + except ValueError: + index = 0 value = setting.choice_list[index] elif setting.type == int: value = 0 @@ -226,7 +294,8 @@ def update_with_settings_string(self, text: str) -> None: value = [item for item in setting.choice_list if item not in value] break - value.append(setting.choice_list[index-1]) + if 0 < index <= len(setting.choice_list): + value.append(setting.choice_list[index - 1]) cur_bits = bits[:setting.bitwidth] bits = bits[setting.bitwidth:] else: @@ -236,8 +305,6 @@ def update_with_settings_string(self, text: str) -> None: setattr(self, 'starting_items', {}) # Settings string contains the GUI format, so clear the current value of the dict format. self.distribution.reset() # convert starting_items - self.settings_string = self.get_settings_string() - self.numeric_seed = self.get_numeric_seed() def get_numeric_seed(self) -> int: # salt seed with the settings, and hash to get a numeric seed @@ -309,7 +376,7 @@ def remove_disabled(self) -> None: setattr(self, info.name, new_value) self._disabled.add(info.name) - self.settings_string = self.get_settings_string() + self._refresh_settings_strings() self.numeric_seed = self.get_numeric_seed() def resolve_random_settings(self, cosmetic: bool, randomize_key: Optional[str] = None) -> None: @@ -371,14 +438,13 @@ def to_json(self, *, legacy_starting_items: bool = False) -> dict[str, Any]: settings = self return { # TODO: This should be done in a way that is less insane than a double-digit line dictionary comprehension. setting.name: ( - {name: ( - {name: record.to_json() for name, record in record.items()} if isinstance(record, dict) else record.to_json() - ) for name, record in getattr(settings, setting.name).items()} + {name: _jsonify_starting_item_record(record) + for name, record in getattr(settings, setting.name).items()} if setting.name == 'starting_items' and not legacy_starting_items else getattr(settings, setting.name) ) for setting in self.setting_infos.values() - if setting.shared and ( + if (setting.shared or setting.visual_shared) and ( setting.name not in self._disabled or # We want to still include settings disabled by randomized settings options if they're specified in distribution ('_settings' in self.distribution.src_dict and setting.name in self.distribution.src_dict['_settings'].keys()) @@ -399,7 +465,9 @@ def get_settings_from_command_line_args() -> tuple[Settings, bool, str, bool, st parser.add_argument('--gui', help='Launch the GUI', action='store_true') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') parser.add_argument('--settings_string', help='Provide sharable settings using a settings string. This will override all flags that it specifies.') + parser.add_argument('--visual_settings_string', help='Visual settings string (seed-neutral).') parser.add_argument('--convert_settings', help='Only convert the specified settings to a settings string. If a settings string is specified output the used settings instead.', action='store_true') + parser.add_argument('--convert_visual_settings', help='Only convert the specified settings to a VISUAL settings string. If a visual settings string is specified output the used visual settings instead.', action='store_true') parser.add_argument('--settings', help='Use the specified settings file to use for generation') parser.add_argument('--settings_preset', help="Use the given preset for base settings. Anything defined in the --settings file or the --settings_string will override the preset.") parser.add_argument('--seed', help='Generate the specified seed.') @@ -449,13 +517,29 @@ def get_settings_from_command_line_args() -> tuple[Settings, bool, str, bool, st settings.output_settings = args.output_settings if args.settings_string is not None: - settings.update_with_settings_string(args.settings_string) + settings.update_with_gameplay_settings_string(args.settings_string) + + if args.visual_settings_string is not None: + settings.update_with_visual_settings_string(args.visual_settings_string) + if args.seed is not None: settings.update_seed(args.seed) settings.custom_seed = True - if args.convert_settings: + if args.convert_settings or args.convert_visual_settings: + if args.convert_visual_settings: + if args.visual_settings_string is not None: + settings.update_with_visual_settings_string(args.visual_settings_string) + out = { + info.name: getattr(settings, info.name) + for info in settings.setting_infos.values() + if getattr(info, "visual_shared", False) and info.bitwidth > 0 + } + print(json.dumps(out)) + else: + print(settings.get_visual_settings_string()) + sys.exit(0) if args.settings_string is not None: # used by the GUI which doesn't support the new dict-style starting items yet print(json.dumps(settings.to_json(legacy_starting_items=True))) diff --git a/SettingsList.py b/SettingsList.py index c3ca47d07b..2703f91413 100644 --- a/SettingsList.py +++ b/SettingsList.py @@ -62,7 +62,8 @@ class SettingInfos: gui_tooltip = '''\ Sets the language used within the game. ''', - shared = True, + shared = False, + visual_shared = True, ) # GUI Only Buttons/Text @@ -398,7 +399,8 @@ class SettingInfos: user_message = Textinput( gui_text = "User-Configurable Message", - shared = True, + shared = False, + visual_shared = True, gui_tooltip = """\ Add a custom message to the seed info. """, @@ -3155,7 +3157,8 @@ class SettingInfos: 'Maps/Compasses: Start With': The dungeon information is available immediately from the dungeon menu. ''', - shared = True, + shared = False, + visual_shared = True, gui_params = { 'randomize_key': 'randomize_settings', }, @@ -3807,7 +3810,8 @@ class SettingInfos: Boss keys will remain in their fancy chest, while small key will be in a smaller version of the fancy chest. ''', - shared = True, + shared = False, + visual_shared = True, disable = { 'off': {'settings': ['minor_items_as_major_chest', 'chest_textures_specific']}, 'classic': {'settings': ['chest_textures_specific']}, @@ -3830,7 +3834,8 @@ class SettingInfos: Any unchecked option will make all items in the category appear in brown chests. ''', - shared = True, + shared = False, + visual_shared = True, gui_params = { "hide_when_disabled": True, }, @@ -3842,7 +3847,8 @@ class SettingInfos: Textures for chests will only be correct when Stone of Agony is found. ''', - shared = True, + shared = False, + visual_shared = True, gui_params = { "hide_when_disabled": True, }, @@ -3877,7 +3883,8 @@ class SettingInfos: Lens of Truth. Lens is not logically required for normally visible chests. ''', - shared = True, + shared = False, + visual_shared = True, ) correct_potcrate_appearances = Combobox( @@ -3907,7 +3914,8 @@ class SettingInfos: original texture once the item is collected. Beehives will wiggle until their item is collected. ''', - shared = True, + shared = False, + visual_shared = True, disable = { '!textures_content': {'settings': ['potcrate_textures_specific', 'soa_unlocks_potcrate_texture']}, }, @@ -3919,7 +3927,8 @@ class SettingInfos: Small keys and boss keys (not key rings) will use custom models to match their dungeon. ''', - shared = True, + shared = False, + visual_shared = True, ) potcrate_textures_specific = MultipleSelect( @@ -3937,7 +3946,8 @@ class SettingInfos: Any unchecked option will make all items in the category appear in regular pots/crates. ''', - shared = True, + shared = False, + visual_shared = True, gui_params = { "hide_when_disabled": True, }, @@ -3949,7 +3959,8 @@ class SettingInfos: Textures for pots and crates will only be correct when Stone of Agony is found. ''', - shared = True, + shared = False, + visual_shared = True, gui_params = { "hide_when_disabled": True, }, @@ -4004,7 +4015,8 @@ class SettingInfos: key text, Good Deal! items sold in shops, random price scrubs, chicken count and poe count. ''', - shared = True, + shared = False, + visual_shared = True, ) damage_multiplier = Combobox( @@ -4296,7 +4308,8 @@ class SettingInfos: 'Anything': Ice Traps may appear as anything. ''', - shared = True, + shared = False, + visual_shared = True, ) # Cosmetics diff --git a/SettingsToJson.py b/SettingsToJson.py index 0dddae3ef0..2401d757a4 100755 --- a/SettingsToJson.py +++ b/SettingsToJson.py @@ -72,6 +72,7 @@ def get_setting_json(setting: str, web_version: bool, as_array: bool = False) -> 'tooltip': remove_trailing_lines('
'.join(line.strip() for line in setting_info.gui_tooltip.split('\n'))), 'type': setting_info.gui_type, 'shared': setting_info.shared, + 'visual_shared': getattr(setting_info, 'visual_shared', False), } if as_array: @@ -198,6 +199,8 @@ def get_section_json(section: dict[str, Any], web_version: bool, as_array: bool for setting in section['settings']: setting_json = get_setting_json(setting, web_version, as_array) + if setting_json is None: + continue if as_array: section_json['settings'].append(setting_json) else: