Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
95576e6
Rough first pass
LinkofOrigin Apr 2, 2025
5b10aeb
Fix CI errors
LinkofOrigin Apr 7, 2025
0c2ff6e
Update remaining setting types
LinkofOrigin Apr 7, 2025
2b34c73
Remove testing code
LinkofOrigin Apr 7, 2025
0a7adcd
Revert testing change
LinkofOrigin Apr 7, 2025
899f42f
Change element visibility checks to a function call
LinkofOrigin Apr 7, 2025
d0679d9
Continued decoupling of visibility and enabling logic
LinkofOrigin Apr 7, 2025
9ec057c
Fix comment (I think?)
LinkofOrigin Apr 7, 2025
b66deda
Comments for the visibility logic
LinkofOrigin Apr 7, 2025
62f83e4
Expand functionality for conditional controls
LinkofOrigin Apr 8, 2025
b301132
Rename to "conditional_controls"
LinkofOrigin Apr 8, 2025
66c9199
Updating naming in architecture file
LinkofOrigin Apr 8, 2025
b4bd1ff
Fix logic to actually enforce disable logic over conditional
LinkofOrigin Apr 8, 2025
2b41ead
Add a global variable for looking up setting state
LinkofOrigin Apr 8, 2025
25b59e0
Refactor conditions processing and setting state
LinkofOrigin Apr 9, 2025
bfad649
Light cleanup
LinkofOrigin Apr 9, 2025
ad27b3b
Revert SettingsList testing changes
LinkofOrigin Apr 9, 2025
54db250
Merge branch 'OoTRandomizer:Dev' into add-conditional-visibility-rule…
LinkofOrigin Jun 11, 2025
945d2c0
Minor refactor and some bug fixes
LinkofOrigin Jun 12, 2025
44c292b
Updating architecture notes to reflect new Conditional Controls
LinkofOrigin Jun 12, 2025
edf61bc
Cleanup and improvements to the docs
LinkofOrigin Jun 12, 2025
efd01c1
Update name for global Settings variable (debugging only)
LinkofOrigin Mar 23, 2026
5dfdfe1
Remove note in architecture for (currently) non-functional option
LinkofOrigin Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 44 additions & 44 deletions GUI/src/app/pages/generator/generator.component.html

Large diffs are not rendered by default.

145 changes: 136 additions & 9 deletions GUI/src/app/pages/generator/generator.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ export class GeneratorComponent implements OnInit {
return;

//Check setting is enabled first
if (!this.global.generator_settingsVisibilityMap[setting.name])
if (!this.settingIsEnabled(setting.name))
return;

event.dataTransfer.dropEffect = 'link'; //Change cursor to link icon when in input area
Expand All @@ -771,7 +771,7 @@ export class GeneratorComponent implements OnInit {
return;

//Check setting is enabled first
if (!this.global.generator_settingsVisibilityMap[setting.name])
if (!this.settingIsEnabled(setting.name))
return;

let items = event.dataTransfer.items;
Expand Down Expand Up @@ -1075,13 +1075,29 @@ export class GeneratorComponent implements OnInit {
return typeof (variable);
}

settingIsEnabled(setting_name: string) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this doesn't do anything different in its current state, this opens up possibilities in the (near?) future to more dynamically control what defines a setting as "enabled" or "disabled".

return this.global.generator_settingsVisibilityMap[setting_name];
}

settingIsFullyHidden(setting: any) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, this gives more control over what determines if a setting is "visible" or "hidden".

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very much like this. Much cleaner, as much as I prefer to control as much as possible directly in Angular HTML

return !this.settingIsEnabled(setting.name) && setting.hide_when_disabled;
}

getSettingCurrentState(setting: any) {
return {
"value": this.global.generator_settingsMap[setting.name],
"visible": !this.settingIsFullyHidden(setting),
"enabled": this.settingIsEnabled(setting.name),
};
}

getNextVisibleSetting(settings: any, startingIndex: number) {

if (settings.length > startingIndex) {
for (let i = startingIndex; i < settings.length; i++) {
let setting = settings[i];

if (this.global.generator_settingsVisibilityMap[setting.name] || !setting.hide_when_disabled)
if (!this.settingIsFullyHidden(setting))
return setting;
}
}
Expand Down Expand Up @@ -1182,7 +1198,7 @@ export class GeneratorComponent implements OnInit {
this.triggerTabVisibility(targetSetting, targetValue);
}

if ("controls_visibility_setting" in targetSetting) {
if ("controls_visibility_setting" in targetSetting || 'conditionally_controls_setting' in targetSetting) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New flag to be processed in the JSON. If this isn't added, then both "disable" logic and "conditional" logic will only get processed if the setting option already has "disable" logic. In other words, a setting option that can only conditionally alter other settings would never have those conditions processed.

triggeredChange = this.triggerSettingVisibility(targetSetting, targetValue, triggeredChange);
}

Expand Down Expand Up @@ -1248,11 +1264,11 @@ export class GeneratorComponent implements OnInit {
let enabledChildren = false;

//If a setting gets disabled, re-enable all the settings that this setting caused to deactivate. The later full check will fix any potential issues
if (targetValue == false && this.global.generator_settingsVisibilityMap[setting.name] == true) {
if (targetValue == false && this.settingIsEnabled(setting.name) == true) {
enabledChildren = this.clearDeactivationsOfSetting(setting);
}

if ((targetValue == true && this.global.generator_settingsVisibilityMap[setting.name] == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
if ((targetValue == true && this.settingIsEnabled(setting.name) == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
triggeredChange = true;

this.global.generator_settingsVisibilityMap[setting.name] = targetValue;
Expand All @@ -1263,7 +1279,19 @@ export class GeneratorComponent implements OnInit {
return triggeredChange;
}

// targetSetting = The current option of the setting to process.
// targetValue = 'true' if the settings this option controls should be enabled, 'false' if they should be disabled.
// (Note: This is passed in 'checkVisibility' as "option != value", in other words: "This option is NOT the option the setting is being changed to".)
// triggeredChange = Set to 'true' to force this function to return 'true', suggesting a change occurred regardless of how things processed.
// Otherwise, the function will return 'true' if a dependent setting's state was altered, otherwise it will return 'false'.
Comment on lines +1282 to +1286
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding on these values waffled for a long time as I kept confusing myself on what each variable represented. Finally just wrote it down so I could stop trying to remember or guess.

private triggerSettingVisibility(targetSetting: any, targetValue: boolean, triggeredChange: boolean) {
// Resolve logic that could conditionally update this setting.
let conditionalSettingUpdates = this.getConditionallyChangedSettingsForOption(targetSetting);

// NOTE: We are treating any setting under "controls_visibility_setting" as one
// that should be disabled by the current option. Could be worth renaming...
let settingsDisabled = []; // Setting names in here are being disabled and take priority over any changes made by conditional logic
if (targetSetting["controls_visibility_setting"] != null) {
targetSetting["controls_visibility_setting"].split(",").forEach(setting => {

//Ignore settings that don't exist in this specific app
Expand All @@ -1272,19 +1300,118 @@ export class GeneratorComponent implements OnInit {

let enabledChildren = false;

if (targetValue == false && this.global.generator_settingsVisibilityMap[setting] == true) {
// We are about to disable this setting.
// If this is currently enabled, attempt to re-enable any settings that it
// may be disabling on its own. If it's disabled, it shouldn't also disable other settings.
if (targetValue == false && this.settingIsEnabled(setting)) {
enabledChildren = this.clearDeactivationsOfSetting(this.global.findSettingByName(setting));
settingsDisabled.push(setting);
}

if ((targetValue == true && this.global.generator_settingsVisibilityMap[setting] == false) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
// We are about to enable this setting.
// If this setting is currently disabled, note that we are triggering a change.
// Alternatively, if disabling this setting causes any other settings to be
// enabled due to it being disabled, then also note that we are triggering a change.
if ((targetValue == true && !this.settingIsEnabled(setting)) || (enabledChildren)) //Only trigger change if a (sub) setting gets re-enabled
triggeredChange = true;

// targetValue = true => This setting will be enabled.
// targetValue = false => This setting will be disabled.
this.global.generator_settingsVisibilityMap[setting] = targetValue;
});
}

// If a setting won't be forcibly disabled, allow conditions to update the setting
for (let settingName in conditionalSettingUpdates) {
if (!settingsDisabled.includes(settingName)) {
this.global.generator_settingsMap[settingName] = conditionalSettingUpdates[settingName]['value'];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I'm not entirely sure about. Did you test if conditional updating of settings is properly stored in presets and it works when loading them back? Does the py safeguard against outputting a valid JSON if the user changes the settings type but leaves an incompatible conditional value setter in the list e.g. change from number to enum but there is a still a conditional number "value" setting? Additionally is it ensured the "default" value in the JSON is always correct e.g. assuming the ultimate default state one day becomes one where some settings are already conditionally disabled forcing other settings to a specific value, but the "default" is set to something else, is that possible and/or an potential issue?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I did just now test each of these a moment ago, I'm also answering much this purely off of memory, so apologies if any of this comes off as facetious:

Did you test if conditional updating of settings is properly stored in presets and it works when loading them back?

I don't recall testing this in the past, but a quick test now with my given example suggests it is working. If I load the S8 Tournament preset, I get "Forest = Closed Deku" and "Starting Age = Random". If I then load the Default / Beginner preset, I get "Forest = Closed Forest" and "Starting Age = Child (disabled)". This is what I would expect once all the logic runs and does it's thing.

The tricky part about this one is that a preset is likely to (or even should?) define a value for every single setting. In other words, there are no settings missing an explicit value as determined by the preset. So even if a setting does have a conditional rule for a value that is set by the preset, the value may not be logically driven by the condition so much as driven by the preset itself.

The parts that would be driven by the conditional logic are the visibility and enabled status, which do appear to work from cursory testing. If we want to test the scenario where a preset doesn't explicitly set a value for a setting that should have its value set by some condition, we'd have to hack up a scenario that likely isn't valid to begin with. (Tell me if I'm wrong though, there's certainly some assumptions in there worth validating)

Does the py safeguard against outputting a valid JSON if the user changes the settings type but leaves an incompatible conditional value setter in the list e.g. change from number to enum but there is a still a conditional number "value" setting?

Pretty confident I did not test this scenario previously, but I can say from testing right this moment that the Python script definitely explodes when trying to run SettingsToJson.py with an invalid type - which is good! I suspect this is because somewhere it tries to cast the setting value to the type assigned to the setting itself (which is maintained in SettingsTypes.py), so if the type doesn't cast properly it throws an error. I also just tested that even changing the value to a non-existent value within a discrete list while retaining the type (eg. changing "closed" to "BIPPITYBOPPITYBOO") also results in an error.

^ That is all outside of any conditional logic as well, just purely the settings values as they are. Though I won't pretend that means the safeguard will cover 100% of the scenarios opened by these changes, but at a basic level it seems to cover some. Worth some extra attention to see if we can find any odd edge cases (eg. maybe testing 0 for a boolean type or something).

(I would also lightly challenge that any issues in this area may be "out of scope" for this particular change. Not to say that we can't - or even shouldn't - fix any issues like this with this PR as needed. Just that if we do uncover any "python json validation" bugs may be pre-existing in some capacity. If we do find some, it would be worth confirming if they are due to this change specifically -vs- if they can be replicated on main or elsewhere)

Additionally is it ensured the "default" value in the JSON is always correct e.g. assuming the ultimate default state one day becomes one where some settings are already conditionally disabled forcing other settings to a specific value, but the "default" is set to something else, is that possible and/or an potential issue?

Also very likely didn't test this before, though let me make sure I'm following here. If I understand things correctly, I think this is the flow/expectation:

  1. If it is set, a "default" value is always a value within the subset list of values of a setting.
  2. Even for those without an explicit default value, the object type on the setting defines a default (or at least should). Therefore every setting should always have at least some value by default.
  3. ^ Per those points, a setting will always have a value - regardless of how it was derived, it will be set to something.
  4. A default value is only applied when there is no value currently recorded in the locally saved settings files (settings.sav) as the GUI loads.
  5. From an order-of-operations perspective, values for settings are always "registered" before the disable/conditional logic is run. (This is currently the case and importantly reliant on point 3 above)

Hopefully that all is accurate and makes some amount of sense. Now to set the stage for your scenario with a specific example (which I just used to test with):

  1. "Starting Age" is conditionally set to "Child" if the "Forest" setting is set to "Closed Forest".
  2. The default value for "Starting Age" is currently set to "Child".
  3. The default value for "Forest" is currently set to "Closed Forest".
  4. Now, I'm going to change the default value for "Starting Age" to be "Adult" instead.

So I update the setting via SettingsToJson -> delete my saved savings file -> re-run the GUI from a fresh state. As of this exact moment, this scenario seems to work correctly. On load, the "Starting Age" value is set to "Child" due to the condition on the "Forest" setting - even though that wasn't the default defined by the base settings list.

Given the first set of bullet points, I think that makes sense. The values are all registered before the conditional logic runs, and (at the risk of stating the obvious) the conditional logic does run in an on-load state with no settings saved locally. If you debugged the loading path, I bet you could see the Starting Age get set to "Adult" initially (or at least saved in a payload of sorts), and then change to "Child" after the other logic runs.

Further, from point 4 in the first set of notes, changing a default value after this point has no effect since the settings have been saved locally already (this happens immediately on first load). The values are set and the conditional logic will run off of those instead. So at a glance this scenario seems covered (again, outside of hacking up a likely invalid scenario just for the sake of it).

this.global.generator_settingsVisibilityMap[settingName] = conditionalSettingUpdates[settingName]['enabled'];
// TODO: Revisit for 'visible' when/if the "visibility" and "enabled" logic are more decoupled and we have more direct control. (See 'settingIsEnabled' and 'settingIsFullyHidden')
triggeredChange = true;
}
}

return triggeredChange;
}

private getConditionallyChangedSettingsForOption(settingOption: any) {
let conditionalSettingUpdates = {};
if (settingOption["conditionally_controls_setting"] != null) {
settingOption["conditionally_controls_setting"].forEach(setting => {

let dependentSetting = this.global.findSettingByName(setting);
if (dependentSetting.conditional_controls != null) {
let targetSettingState = this.getTargetSettingStateFromConditions(dependentSetting);
let currentSettingState = this.getSettingCurrentState(dependentSetting);

// If any part of the setting would change, save the new setting state for later
if (currentSettingState['value'] != targetSettingState['value'] ||
currentSettingState['enabled'] != targetSettingState['enabled'] ||
currentSettingState['visible'] != targetSettingState['visible']
) {
conditionalSettingUpdates[dependentSetting.name] = targetSettingState;
}
}
});
}
return conditionalSettingUpdates;
}


private getTargetSettingStateFromConditions(setting: any) {
// Start with the current state as the target
// If no conditions change the target state, then we effectively just return the current state
let targetSettingState = this.getSettingCurrentState(setting);

// There may be multiple combinations of conditions that may alter this setting.
// We'll check each one, and if one of them passes we'll use that to determine the setting's state.
let settingConditions = setting.conditional_controls;
let conditionHasDisabled = false;
for (let conditionName in settingConditions) {
var conditionToTest = settingConditions[conditionName];
let conditionList = conditionToTest['conditions'];
let conditionsPassed = [];
for (let i = 0; i < conditionList.length; i++) {
let condition = conditionList[i];
let partialConditionPassed = false;
// Only one of these conditional settings has to match the given value
for (let conditionalSettingName in condition) {
// If the conditional setting is currently set to the conditional value...
if (condition[conditionalSettingName] == this.global.generator_settingsMap[conditionalSettingName]) {
partialConditionPassed = true;
break;
}
}

conditionsPassed.push(partialConditionPassed);
};

// If one full condition passed, we'll use that condition's target state
if (!conditionsPassed.includes(false)) {
// TODO: Define priority rules so we know what should take precedent.
// - Option 1: First that passes has priority => just early exit
// - Option 2: Last that passes has priority => could result in mixed data sets if "condition1" sets some of the state and later "condition 3" sets other parts
// - Option 3: Manually define priority inside the blob => basically option 1 with extra logic. But what if two options have the same priority? First or last wins?
// If the condition sets one of these keys, we'll use that value. Otherwise use the current value.
targetSettingState['value'] = conditionToTest['value'] != null ? conditionToTest['value'] : targetSettingState['value'];
targetSettingState['enabled'] = conditionToTest['enabled'] != null ? conditionToTest['enabled'] : targetSettingState['enabled'];
targetSettingState['visible'] = conditionToTest['visible'] != null ? conditionToTest['visible'] : targetSettingState['visible'];
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT: This 'visible' flag currently has no effect. This is because currently the only control over visibility is the hide_when_disabled flag which interlocks the relationship between a setting being "enabled" and being "visible". If we want this flag to work as a separate bit of control, then we'll need to also update that logic - which is doable but somewhat awkward.

I'm on the fence about removing this option entirely. It could be nice to have it ready to roll, but part of me also doesn't like the idea of having a flag that could be set which does absolutely nothing. Could result in some confusion if someone tries to set in in SettingsList and it does nothing...

That said, I'm reasonably confident that anyone updating settings there has a 99% chance to just use what's already in SettingsList for reference of existing flags. So I don't expect anyone to get confused if they just copy/paste/edit based on existing content in there.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you marked it as ToDo "currently not working" then it's fine to leave it like this, as long as the SettingsList.py makes no mention of it and it doesn't go into any settings documentation. People indeed probably won't look in here to notice and just go off the existing SettingsList

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I think we're on the same page then. This change is very intentionally not changing anything with the actual SettingsList right now as an attempt to just "add this feature, utilize it later". I suspect the biggest reason this might come up is that once people are using the conditional controls someone might think "hmm, you know it'd be nice if..." - at which point a dev would reasonably revisit this exact code and can start discussions/make decisions from there.

Can always adjust though, easy enough to comment out/delete if there ends up being a stronger reason to do so 👍

if (targetSettingState['enabled'] == false) {
conditionHasDisabled = true;
}
break; // First condition that passes wins and takes priority
}
}

// The setting is currently disabled, but no conditions are attempting to disable it.
// Let's re-enable it and the old "disable" logic can take priority if needed.
if (!conditionHasDisabled && targetSettingState['enabled'] == false) {
targetSettingState['enabled'] = true;
}

return targetSettingState;
}

clearDeactivationsOfSetting(setting: any) {

let enabledChildren = false;
Expand Down Expand Up @@ -1319,7 +1446,7 @@ export class GeneratorComponent implements OnInit {

this.global.getGlobalVar('generatorSettingsArray').forEach(tab => tab.sections.forEach(section => section.settings.forEach(checkSetting => {

if (skipSetting && checkSetting.name === skipSetting || !this.global.generator_settingsVisibilityMap[checkSetting.name]) //Disabled settings can not alter visibility anymore
if (skipSetting && checkSetting.name === skipSetting || !this.settingIsEnabled(checkSetting.name)) //Disabled settings can not alter visibility anymore
return;

if (checkSetting["type"] === "Checkbutton" || checkSetting["type"] === "Radiobutton" || checkSetting["type"] === "Combobox" || checkSetting["type"] === "SearchBox") {
Expand Down
23 changes: 23 additions & 0 deletions GUI/src/app/providers/GUIGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,15 @@ export class GUIGlobal implements OnDestroy {

async parseGeneratorGUISettings(guiSettings, userSettings) {
const isRGBHex = /[0-9A-Fa-f]{6}/;
/*
!! DEBUGGING ONLY !!
This value can be accessed via the browser console as simply 'SettingsListZOOTR_Angular'
and is only intended for debugging. The current list of settings as they appear in the UI
will be accessible via this variable. Their values will be objects representing their current
state and metadata as is relevant, including a direct JSON-decoded object of that settings'
data from the generated settings list JSON file.
*/
globalThis.SettingsListZOOTR_Angular = {};
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated name - open to suggestions because #NamingIsHard


//Intialize settings maps
for (let tabIndex = 0; tabIndex < guiSettings.settingsArray.length; tabIndex++) {
Expand Down Expand Up @@ -528,6 +537,20 @@ export class GUIGlobal implements OnDestroy {

this.generator_settingsVisibilityMap[setting.name] = true;

// Bind a property as a function that returns an object representing this setting for easy debugging
// By using the setting name as the property name, it allows for auto-complete and fuzzy searching/suggestions
// This works by binding a property to a getter function that has the current 'this' value bound to the function context
Object.defineProperty(globalThis.SettingsListZOOTR_Angular, setting.name, {
get: () => {
return {
// Object representing the current state of this setting. Add more values as you see fit.
enabled: this.generator_settingsVisibilityMap[setting.name],
value: this.generator_settingsMap[setting.name],
_json: this.findSettingByName(setting.name),
};
},
});

if (setting.type == "SearchBox" && userSettings && setting.name in userSettings) { //Special parsing for SearchBox data

let valueArray = [];
Expand Down
7 changes: 7 additions & 0 deletions Notes/GUI/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ utils/settings_list.json contains every single setting of the GUI as an array of
4. controls-visibility-tab: What tab(s) to disable when this option is selected. Multiple tabs can be separated by comma and are addressed by their internal name (see mapping.json structure)
5. controls-visibility-section: What section(s) to disable when this option is selected
6. controls-visibility-setting: What specific setting(s) to disable when this option is selected
* conditional-controls &rarr; An object of "conditions" that can alter this setting's visibility, enabled state, and value based on the state of other settings. This object can contain multiple items that each define their own conditions and target state for the setting. The first condition that passes will take priority in altering the setting and the other conditions will not be evaluated. If this setting would be disabled by some other logic (eg. 'controls_visibility_setting'), that will take priority over ALL condition-based logic. When this happens, the setting state will not be changed even if a passing condition would normally do so. The format of a condition object is as follows:
* "key" -> {object}: "key" has no functional purpose and is purely for human readability and debugging purposes. "object" contains key/value pairs that define the behavior of the condition.
* value: If the condition passes, the setting will be changed to this value.
* enabled: If the condition passes, `True` will enable the setting and `False` will disable it.
* conditions: A list of "partial condition" objects that determine if this condition passes or not. All "partial conditions" must pass for the full condition to also pass. (This provides `AND` logic)
* Each partial condition contains "key" -> "value" pairs in the format of "setting_name" -> "setting_value". If at least one of these pairs matches the current state of the settings, the partial condition will pass. Otherwise it will fail. (This provides `OR` logic)

### The settings_mapping.json structure

Expand Down Expand Up @@ -81,6 +87,7 @@ The settings array follows that defines the settings that should appear in this
* controls-visibility-tab &rarr; What tab(s) to disable when this setting is enabled, used for Checkbuttons. Multiple tabs can be separated by comma and are addressed by their internal name
* controls-visibility-section &rarr; What section(s) to disable when this setting is enabled
* controls-visibility-setting &rarr; What specific setting(s) to disable when this setting is enabled
* conditional-controls &rarr; List of setting/value pairs this setting may be dependent on to determine what its current state should be (eg. disabled, specific value, etc.)
* hide-when-disabled &rarr; If this setting should be completely hidden when it gets disabled, not just greyed out. Used on the website to make the difference between generator and patcher more distinct
* min &rarr; The minimum numeric value allowed. Used for Scales and Numberinputs
* max &rarr; The maximum numeric value allowed. Used for Scales and Numberinputs. Can differ between Electron and website (e.g. multi world limit)
Expand Down
Loading
Loading