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}}
+
+
+
+
+
+
@@ -285,6 +291,16 @@ {{section.text}}
+
+
+
+
+
+
+
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: