Allows you to create mod configuration presets for Don't Starve Together server and client mods.
ServerCreationScreen creates WorldSettingsTab creates WorldSettingsMenu creates PresetBox
- Revert
- Update
- OnEditPreset opens
NamePresetScreenwhich calls EditPreset - If you have selected a custom preset, this button will allow you to update the settings, name, and description associated with that preset.
- OnEditPreset opens
- Choose settings preset
- Opens
PresetPopupScreen - This button allows you to select a preset from a list of presets. (There may be built-in presets that you cannot alter or delete.)
- Opens
- Save as new preset
- OnSavePreset opens
NamePresetScreenwhich calls SavePreset - This opens up a screen that allows you to provide a name and description for a new preset that stores the current state of the configuration options.
- OnSavePreset opens
-
Update
- EditPreset opens
NamePresetScreenwhich calls oneditfn- oneditfn, ondeletefn, and onconfirmfn are functions that are passed into the
PresetPopupScreenconstructor byPresetBox.
- oneditfn, ondeletefn, and onconfirmfn are functions that are passed into the
- This button allows you to change the name or description of a preset, but it DOES NOT update the preset's mod configurations, unlike the
PresetBoxedit/update button.
- EditPreset opens
-
Delete
- DeletePreset calls ondeletefn
- This shows a confirmation screen and will delete the associated preset from the stored presets upon approval.
-
Apply
- OnConfirmPreset calls onconfirmfn which calls the
PresetBoxOnPresetChosen function. - This button will apply the configuration options of the selected preset. The applied preset will then become the "currentpreset".
- OnConfirmPreset calls onconfirmfn which calls the
PresetPopupScreen Note: If a currentpreset exists, that preset will be the default item selected and highlighted with a darker color. Otherwise, the first item in the list will be selected and no item will be highlighted.
- We'd eventually like to add an easier way to import and export mod presets, that way two friends can easily share their mod setup.
- What happens if someone gives you a mod preset that references mods you don't have? (Use Klei's TheSim:StartDownloadTempMods or TheSim:SubscribeToMod(mod.mod_name) or GLOBAL.DownloadMods? All found in networking.lua)
- Potentially make this mod compatible with our In-Game Mod Manager that allows you to change client mods while you're in-game.
The following screens are modified or used by this mod
ServerCreationScreen(class post construct creates a modifiedPresetBoxonModsTab)PresetPopupScreen(lists available presets)- OnPresetButton (for highlighting/selecting a preset in the list)
- EditPreset (edits only name and description of a preset)
- DeletePreset (asks for confirmation, then calls
PresetBox.DeletePreset)
NamePresetScreen(confirmation popup for editing preset details)
The following widgets are modified or used by this mod
ModsTab(PresetBoxis added as a child)PresetBox- OnSavePreset (save new preset while on
ModsTab) - OnPresetButton (creates and modifies
PresetPopupScreen) - OnPresetChosen (asks for confirmation, then applies a mod preset)
- OnEditPreset (updates a preset while on
ModsTab) - DeletePreset (deletes a preset from
presetstable and mod_presets file)
- OnSavePreset (save new preset while on
- currentpreset: This refers to the most recently applied preset on the configuration options. This mod does not remember the preset last applied for each save slot, so this variable will always be initialized to a dummy value (see "Choose Mod Preset" section).
- selectedpreset: Only applies to
PresetPopupScreenand refers to the item in the list that was last clicked. There MUST be a selected preset at all times. Clicking the Apply button with no selected preset will cause a crash.
To start, since Klei already provided a lot of functionality for us (such as the preset box and the preset list and all their corresponding buttons) this mod is really just comprised of a TON of overrides (what mod isn't?).
Before I start making a detailed outline of the code, I'll describe how we SAVE the presets first so the mod's structure will make sense.
We started by trying to figure out how to retrieve a list of enabled mods and their respective configuration options for a specific save slot. We learned that KnownModIndex contains many functions that allow us to not only get information about enabled mods and configs, but also to set them. We figured out how to get a list of enabled mods (with ModManager:GetEnabledServerModNames()) and their config choices (with KnownModIndex:LoadModConfigurationOptions(modname, is_client_mod)). These loaded configuration options must be deep copied since the config object returned can still be altered elsewhere.
Once we learned how to collect the mod information, we figured we could use Klei's SavePersistentString function to store the data to a file somewhere on the disk. By default, SavePersistentString saves files in here: C:\Users\...\Documents\Klei\DoNotStarveTogether\254404980\client_save
We originally wanted to store the individual presets in their own folder (e.g. \client_save\mod_presets\) in separate files. However, since file manipulation is only really possible in the DST data folder, and we couldn't create a folder in client_save from modmain, and we wanted to use SavePersistentString to save inside the client_save folder, we settled for storing all the presets in one object stored in one file called mod_presets.
When the mod first loads up, it checks if mod_presets exists and has data. If it is empty or nil, then we create the file for the first time and initialize it with our built-in "Vanilla" preset.
Remember: One of the first things the mod does is read the file and store the table in a variable called
presets. As people add and change and delete presets, you can safely assume that thepresetstable and the mod_presets file will be in sync.
We first added a PresetBox "child" on the ModsTab (ModsTab being a child of ServerCreationScreen), and we set its position with a bunch of magic numbers. Since the PresetBox won't start up with a currentpreset, we have to disable the "Update" button because there is nothing to update if no preset is selected. Also, the box that displays the name and description of the current preset will start up empty.
After this, we got the "Save as new preset" button working. This is simply done by overriding PresetBox's OnSavePreset function. We still push a screen to ask for the new name and description of the preset, and then we just run OUR save function instead.
Now, the biggest part of this mod is the override to PresetBox's OnPresetButton. This is the on-click function for the "Choose Mod Preset" button. This button is what calls the PresetPopupScreen into existence, which means it is the only place where we will ever have a reference to the screen object and be able to change its properties.
When "Choose Mod Preset" is clicked, it creates a PresetPopupScreen and its constructor requires:
- The current preset
- We pass currentpreset or something else that is NOT nil (i.e. USE_FIRST_PRESET) in order to bypass a nil check inside the constructor. We need control over this to make sure that our
PresetPopupScreenhad SOMETHING selected when it opens. When it opens, we check if currentpreset is USE_FIRST_PRESET and then we can change it from there.
- We pass currentpreset or something else that is NOT nil (i.e. USE_FIRST_PRESET) in order to bypass a nil check inside the constructor. We need control over this to make sure that our
- An onconfirmfn that will run when "Apply" is selected
- An oneditfn that will run when an "Update Preset" button is clicked on one of the items in the preset list. This will update JUST THE NAME AND DESCRIPTION of a preset
- An ondeletefn that will run when the "Delete" button is clicked on one of the items in the preset list
- The levelcategory (Not used by us, so we just pass in whatever was passed into
PresetBox's constructor. We passed in LEVELCATEGORY.SETTINGS) - Some other level-related parameter
- The location (Not used by us, so we just pass in
presetbox.parent_widget:GetLocation())
Now prepare for something quite programmatically horrifying. In order to generate the list of presets on the PresetPopupScreen the way we needed, we were forced to copy the ScrollWidgetsCtor (ctor for each scroll widget) and ApplyDataToWidget functions pretty much one-for-one from presetpopupscreen.lua. This also means we had to copy all the magic numbers and variables they defined into our class post construct. *Sigh... (If Klei touches these files at all in future updates this mod might be shattered into a million pieces...)
After copying these two functions, we kill the original scroll_list. We generate an array of items (using an array version of the presets object, indexable with numbers instead of ID's) in alphabetical order (except for built-in presets which go on top), and then we re-create the scroll_list widget with this new array and the ScrollWidgetsCtor and ApplyDataToWidget functions.
After we create scroll_list again, we eventually check for the whole currentpreset issue. If selectedpreset is still our USE_FIRST_PRESET placeholder, then just select the first preset from OUR list and not some other junk data that will cause a crash like mentioned earlier.
Overrides for our PresetPopupScreen
- OnPresetButton
- This is the on-click function for each item in the preset list (as specified in ScrollWidgetsCtor). It just updates the selectedpreset and refreshes the list to show an outline around the selected preset. Our override is pretty much the same as the original function, but we had to make sure the presetid was passed into
self:OnSelectPreset(presetid)correctly.
- This is the on-click function for each item in the preset list (as specified in ScrollWidgetsCtor). It just updates the selectedpreset and refreshes the list to show an outline around the selected preset. Our override is pretty much the same as the original function, but we had to make sure the presetid was passed into
- EditPreset
- This function is called when someone wants to update ONLY the name and description of a preset from the preset list. Our override prevents the game from calling more functions related to WORLD presets. It calls the oneditfn we passed earlier which eventually calls oneditpresetdetails which updates the file and the
presetstable. If they confirm their changes, it will update presetsList (array version) and refresh the list view. And if the updated preset happens to be the current preset, oneditfn will update thePresetBoxtext to immediately reflect the change.
- This function is called when someone wants to update ONLY the name and description of a preset from the preset list. Our override prevents the game from calling more functions related to WORLD presets. It calls the oneditfn we passed earlier which eventually calls oneditpresetdetails which updates the file and the
- DeletePreset
- Always shows a confirmation screen. Again, our override prevents the game from calling more "Level" functions related to world settings that will cause a crash. Upon confirmation, our ondeletefn is called which updates the
presetstable and the mod_presets file. (If the deleted preset was the current preset, then clear the text on thePresetBoxand disable the update button.) Then update presetsList (array). If the deleted preset was the selected preset, then select the first item in the list and refresh the list view.
- Always shows a confirmation screen. Again, our override prevents the game from calling more "Level" functions related to world settings that will cause a crash. Upon confirmation, our ondeletefn is called which updates the
The following point partially belongs to PresetBox since it is an override of the PresetBox OnPresetChosen function, but it is only called by PresetPopupScreen's "Apply" button.
- The "Apply" button /
PresetBoxOnPresetChosen - (The fun part)- When "Apply" is clicked, it will always show a confirmation screen before running the onconfirmfn we passed earlier. In order to apply the changes, we need to first disable all mods. We use GetEnabledServerModNames again, and then run a
ModsTabfunctionmods_tab:OnConfirmEnable(restart_required, modname)to toggle them into a disabled state one by one. Next, we go throughpresetsand call the same function to enable the preset's mods. Next we use a wonderful functionKnownModIndex:SaveConfigurationOptions(callback, modname, configs, is_client_mod)to apply the individual configurations to the newly enabled mods. Then we set currentpreset to the preset that was just applied. If the preset is a built-in preset, we disable the update button. Lastly update the text on thePresetBox.
- When "Apply" is clicked, it will always show a confirmation screen before running the onconfirmfn we passed earlier. In order to apply the changes, we need to first disable all mods. We use GetEnabledServerModNames again, and then run a
Note: Something that is very nice about all these
ModManagerandKnownModIndexandModsTabfunctions is that they do not permanently edit the mod settings and configurations. You can simply click the back button on theServerCreationScreento undo all the changes. (This is something we can eventually add to the "Revert Changes" button. Right now it's always disabled.)
Updating a preset from PresetBox will allow you to update the name and description AND the config choices for the currentpreset. Overriding OnEditPreset was pretty straightforward. We just create a NamePresetScreen and run the same function we use to save a new preset. If an entry already exists in the presets table, it will simply overwrite the existing data (see Lua note). After the changes to the preset are saved to the mod_presets file, we update the PresetBox text to immediately reflect the changes if they changed the name or description.
Lua Note: If I assign a new value in a lua table like this:
myTable["keyHere"] = value, I can just run the same code to update the value:myTable["keyHere"] = newValue.
Be sure to check out the code itself in modmain.lua for more notes!
And make sure to check out the GitHub repository: ModPresets - GitHub - rawii22 and albertoromañach
And the Steam Workshop: Mod Presets - Steam