Jump to content

World Options and Mod Changes


zarklord_klei
 Share

Recommended Posts

  • Developer

The March QoL update has added a number of new modding API features, including some suggested by the community!

modservercreationmain.lua
This is a new main file that will run on the server creation screen, when your mod is enabled, Currently this is only used for one thing, frontend asset loading, but in the future, it can be expanded upon more.

Frontend Asset Loading

Asset loading on the frontend is just like asset loading in modmain

--in modservercreationmain.lua
FrontEndAssets = {
	--list of assets to load.
}

because it's possible for the assets you need to change, if they ever change you can call ReloadFrontEndAssets() to reload the frontend assets loaded by your mod.

Adding World Settings/Generation Options

With the split between world settings and world generation options, we now have the concept of categories.

LEVELDATA.SETTINGS --Use this to refer to world settings, stuff changable after worldgen.
LEVELDATA.WORLDGEN --Use this for world generation options.

wherever you see the argument "category" you need to pass it one of these two.
Adding Groups

AddCustomizeGroup(category, name, text, desc, atlas, order)
--category, the category of the group.
--name, the internal name of the group
--text, the name displayed in the menus for this group
--desc, the default spinner description, all items will fallback to this if they lack a specific spinner description OPTIONAL
--atlas, the default atlas for all items in this group OPTIONAL
--order, a number specifiying the sorting order in relation to other groups, leave nil to sort alphanumerically OPTIONAL

Groups are a way of sorting a bunch of similar options into one thing to make it easier to find, all items must be in a group.

Removing Groups

RemoveCustomizeGroup(category,  name)
--category, the category of the group your removing
--name, the internal name of the group your removing

You can only remove groups in the base game, or groups added by your mod, but you can use this to remove groups of options based on things that can occur on the server creation screen.

Adding Items

AddCustomizeItem(category, group, name, itemsettings)
--category, the category of the group
--group, the name of the group to insert your item
--name, the internal name of your item, this MUST be different from all other items, even across categories.
--itemsettings, a table containing the following:
--itemsettings.atlas the atlas of the item
--itemsettings.image, the image of the item
--itemsettings.desc, the spinner descriptions of the item.
--itemsettings.value, the default value of the item, usually "default"
--itemsettings.order, a number specificying the sorting order in relation to the other items in the group, leave nil to sort alphanumerically OPTIONAL
--itemsettings.world, a table containing the locations where this setting should be avaliable(EG "forest" or "cave"), leave nil to enable for all locations.
--itemsettings.master_controlled, if set to true, will only show up for the MASTER location (usually forest), if the location passed the location test.
--itemsettings.masteroption, if set to true, will copy the option to the secondary shard upon starting the world from the server creation screen.

This will add an item to the specified group that users can configure on the server creation screen.

Removing Items

RemoveCustomizeItem(category, name)
--category, the category of the item to remove
--name, the internal name of the item to be removed.

Just like groups, you can only remove items in the base game, or items added by your mod, you can use this to remove options based on things that can occur on the server creation screen.

Getting Customize Descriptions

GetCustomizeDescription(description)
--description, the name of the description to get.

All descriptions used in the base game are available through this command, the valid arguments for this command are:

"frequency_descriptions"
"worldgen_frequency_descriptions"
"ocean_worldgen_frequency_descriptions"
"starting_swaps_descriptions"
"petrification_descriptions"
"speed_descriptions"
"disease_descriptions"
"day_descriptions"
"season_length_descriptions"
"season_start_descriptions"
"size_descriptions"
"branching_descriptions"
"loop_descriptions"
"complexity_descriptions"
"specialevent_descriptions"
"yesno_descriptions"
"extrastartingitems_descriptions"
"autodetect"
"dropeverythingondespawn_descriptions"

You can look at scripts/customize.lua to see the exact values of these spinner descriptions.

Implementing the options

Almost all world settings are implemented the file scripts/worldsettings_overrides.lua. So look there for examples on how implement the world settings.
Almost all world options are implemented via the table TRANSLATE_TO_PREFABS in scripts/forest_map.lua. So look there for examples on how to implement your world generation options,

Putting it all together

To load the assets for your world options, create a file called modservercreationmain.lua in the root of your mod. Then add your images to the assets table. Here is an example of what that might look like:

FrontEndAssets = {
    Asset("IMAGE", "images/world_seed.tex"),
    Asset("ATLAS", "images/world_seed.xml"),
}

To add your world options, create a file called modworldgenmain.lua in the root of your mod, and then add your world options. Again an example of what that might look like:

--add the string for your setting that will be used on the world options screen.
--the path should be STRINGS.UI.CUSTOMIZATIONSCREEN.YOUR_OPTION_NAME_IN_UPPERCASE
STRINGS.UI.CUSTOMIZATIONSCREEN.WORLDSEED = "Leave blank for a random seed."

--this will add the option "worldseed" in the group "misc" that is in the category WORLDGEN.
AddCustomiseItem(LEVELCATEGORY.WORLDGEN, "misc", "worldseed", {order = -10, value = "default", image = "world_seed.tex", atlas = "images/world_seed.xml")
  --an order of -10 will sort it above all the options already in that list, image and atlas are set to the custom atlas I loaded in modservercreationmain.lua


If you have any questions, please ask them, and I'll do my best to answer them.

Happy modding!

  • Like 11
  • Thanks 1
Link to comment
Share on other sites

  • Developer
Just now, TheSkylarr said:

Oh right, haha. I was wondering if this would finally solve the problem of character mods not being able to show their saveslot portrait, but that makes sense.

I do plan on adding something to solve this problem, no promises on when though.

  • Like 1
  • Thanks 4
Link to comment
Share on other sites

Just now, zarklord_klei said:

I do plan on adding something to solve this problem, no promises on when though.

Good to hear! I just made a really janky client mod that does this, but the way I'm doing it is terrible cause I don't have enough experience, so I would love to see another QOL update add this eventually.

Link to comment
Share on other sites

Hey, so I've been experimenting with all this and I'm sure it's gonna make my mod configs much cleaner, sounds neat.

However I'm having some... troubles :

-- string file

GLOBAL.STRINGS.UI.CUSTOMIZATIONSCREEN.CHERRY_TREE = "Cherry Trees"

-- modworldgenmain

AddCustomizeGroup(LEVELCATEGORY.WORLDGEN, "cherry", "Cherry Forest")

AddCustomizeItem(LEVELCATEGORY.WORLDGEN, "cherry", "cherry_tree", {order = 1, value = "default", world = {"forest"}, image = "images/customization/worldgen_cherrytree.tex", atlas = "images/customization/worldgen_cherrytree.xml"})

-- modservercreationmain

FrontEndAssets = {
	Asset("ATLAS", "images/customization/worldgen_cherrytree.xml"),
	Asset("IMAGE", "images/customization/worldgen_cherrytree.tex"),
}

Whatever I've been trying to do there just copy the Starting Season config : its name, image, and options :

worldgen.thumb.jpg.e78902eb0f74026a0e7f05d2341bf999.jpg

Something else, I'm not sure if that is my fault or not but whenever I scroll the game crash :
(only while the configs have mod options of course)

worldgen2.thumb.png.9062bc998427575d76f24004a1437651.png

Link to comment
Share on other sites

Ok, so you implemented a way for mods to add their own world-settings or world generation settings in world creation screen...

But what happens afterwards? In your example you added a worldseed option. But how to read out what option the user picked? And how to change eg the worldseed according to the setting?

And more important for me: How to change exisiting world settings/creation options, like forcing a specific season and other stuff with my mod?
I currently use AddLevelPreInitAny to change the tasksetdata directly ("set_pieces", "required_prefabs" and for settings also "overrides"). Is this still the way to go? Or are there better ways now?

https://steamcommunity.com/sharedfiles/filedetails/?id=1847959350
https://steamcommunity.com/sharedfiles/filedetails/?id=756229217

edit: I have huge problems with my adventure and teleportato mod. it seems AddLevelPreInitAny function triggers twice now. I guess one time for world creation and one time for worldsettings, after world is created. But only in the first case there is a "overrides" I can change. I can successfully change the start_season, but I can not change the season or day length it seems..@zarklord_klei Or what code should I use to change those settings with a mod? 
Also the whole Level and Chapter design does not work anymore, this might be related to the "io.read/write" already mentioned above by Tranoze.

PS:
since you are a very good developer, could you please bump these bug reports:
https://forums.kleientertainment.com/klei-bug-tracker/dont-starve-together/addtaskpreinit-does-not-affect-optionaltasks-intended-r25702/

https://forums.kleientertainment.com/klei-bug-tracker/dont-starve-together/actionspick-not-correct-with-mouseclick-works-with-spacebar-r20816/

 

edit4:
The only working way I found, to change eg season lenght via mod is the following, but I really doubt this is the intended way:

local WorldSettings_Overrides = require("worldsettings_overrides")

-- and within any function that runs after the world was created:
    WorldSettings_Overrides.Post["autumn"]("noseason")

 

Edited by Serpens
added more info
Link to comment
Share on other sites

  • Developer
On 3/12/2021 at 12:55 AM, Tranoze said:

I use GLOBAL.io to write/read custom log file for my mod, and in this update read/write file always return "Bad file descriptor" error.

This should be resolved now 

 

On 3/12/2021 at 6:51 AM, Serpens said:

Ok, so you implemented a way for mods to add their own world-settings or world generation settings in world creation screen...

But what happens afterwards? In your example you added a worldseed option. But how to read out what option the user picked? And how to change eg the worldseed according to the setting?

And more important for me: How to change exisiting world settings/creation options, like forcing a specific season and other stuff with my mod?
I currently use AddLevelPreInitAny to change the tasksetdata directly ("set_pieces", "required_prefabs" and for settings also "overrides"). Is this still the way to go? Or are there better ways now?

https://steamcommunity.com/sharedfiles/filedetails/?id=1847959350
https://steamcommunity.com/sharedfiles/filedetails/?id=756229217

edit: I have huge problems with my adventure and teleportato mod. it seems AddLevelPreInitAny function triggers twice now. I guess one time for world creation and one time for worldsettings, after world is created. But only in the first case there is a "overrides" I can change. I can successfully change the start_season, but I can not change the season or day length it seems..@zarklord_klei Or what code should I use to change those settings with a mod? 
Also the whole Level and Chapter design does not work anymore, this might be related to the "io.read/write" already mentioned above by Tranoze.

PS:
since you are a very good developer, could you please bump these bug reports:
https://forums.kleientertainment.com/klei-bug-tracker/dont-starve-together/addtaskpreinit-does-not-affect-optionaltasks-intended-r25702/

https://forums.kleientertainment.com/klei-bug-tracker/dont-starve-together/actionspick-not-correct-with-mouseclick-works-with-spacebar-r20816/

 

edit4:
The only working way I found, to change eg season lenght via mod is the following, but I really doubt this is the intended way:


local WorldSettings_Overrides = require("worldsettings_overrides")

-- and within any function that runs after the world was created:
    WorldSettings_Overrides.Post["autumn"]("noseason")

 

I will take a look at this on monday.

  • Like 1
  • Thanks 2
Link to comment
Share on other sites

1 hour ago, zarklord_klei said:

This should be resolved now 

 

I will take a look at this on monday.

thank you very much.
For easier work, I will summarize how I used to change genration-settings (this is an "easy" example, copy pasted and merged from my scripts, so may not be functional, but shows what I did previously).

in modworldgenmain :

local function IsModLoaded(modname) -- there is no TheNet on world generation, so we use this to find out about gem api
    return GLOBAL.KnownModIndex:IsModEnabled(modname) or GLOBAL.KnownModIndex:IsModForceEnabled(modname)
end
local GEMAPIActive = IsModLoaded("workshop-1378549454")

AddLevelPreInitAny(function(tasksetdata)
    local _overrides = nil
    if GEMAPIActive then -- to prevent gem api to revert our changes in rare cases
        _overrides = GLOBAL.shallowcopy(tasksetdata.overrides)
    end
    if tasksetdata.location == "forest" then
        tasksetdata.tasks = {"Tentacle-Blocked Spider Swamp"}
        tasksetdata.numoptionaltasks = 0
        tasksetdata.optionaltasks = {}
        tasksetdata.set_pieces = {}
        tasksetdata.required_setpieces = {}
        table.insert(tasksetdata.required_setpieces,adventureportal)
        tasksetdata.numrandom_set_pieces = 0
        tasksetdata.random_set_pieces = {}
        tasksetdata.required_prefabs = {"spawnpoint_master","adventure_portal"}
        tasksetdata.ocean_prefill_setpieces = {} -- delete any ocean stuff
        tasksetdata.ocean_population = {} -- delete any ocean stuff
        
        if tasksetdata.overrides==nil then
            tasksetdata.overrides = {}
        end
        tasksetdata.overrides.world_size  =  "small"
        tasksetdata.overrides.wormhole_prefab = "wormhole"
        tasksetdata.overrides.layout_mode = "LinkNodesByKeys"
        tasksetdata.overrides.deerclops  =  (GetModConfigData("difficulty")==0 and "never") or (GetModConfigData("difficulty")==1 and "never") or (GetModConfigData("difficulty")==2 and "rare") or (GetModConfigData("difficulty")==3 and "default") or "never"
        tasksetdata.overrides.dragonfly  =  "never"
        tasksetdata.overrides.bearger  =  (GetModConfigData("difficulty")==0 and "never") or (GetModConfigData("difficulty")==1 and "never") or (GetModConfigData("difficulty")==2 and "rare") or (GetModConfigData("difficulty")==3 and "default") or "never"
        tasksetdata.overrides.goosemoose  =  (GetModConfigData("difficulty")==0 and "never") or (GetModConfigData("difficulty")==1 and "never") or (GetModConfigData("difficulty")==2 and "rare") or (GetModConfigData("difficulty")==3 and "default") or "never"
        tasksetdata.overrides.antlion = "never"
        tasksetdata.overrides.season_start  =  "autumn"
        tasksetdata.overrides.day = "noday"
        tasksetdata.overrides.autumn = "veryshortseason"
        tasksetdata.overrides.winter = "veryshortseason"
        tasksetdata.overrides.spring = "veryshortseason"
        tasksetdata.overrides.summer = "veryshortseason"
        tasksetdata.overrides.keep_disconnected_tiles = true
        tasksetdata.overrides.no_joining_islands = true
        tasksetdata.overrides["has_ocean"] = false
    end
    if GEMAPIActive and _overrides~=nil then -- to prevent gem api to revert our changes in rare cases
        local overrides_to_block = {}
        for k, v in pairs(tasksetdata.overrides) do
            --original blockoverrides updateanyways are 3 things that could be in this table that arent actually overrides.
            if k ~= "original" and k ~= "blockoverrides" and k ~= "updateanyways" and _overrides[k] ~= v then
                table.insert(overrides_to_block, k)
            end
        end
    	GLOBAL.gemrun("overridesblocker", tasksetdata.overrides, modname, overrides_to_block)
    end
end)

All new "world_settings", so the ones you could change after the world was generated, are no longer affected by this code. So no way to change season lenghts, or day length and stuff like this with this code.

And I got "overrides index nil" errors (after recent update), so I had to add these lines:

        if tasksetdata.overrides==nil then
            tasksetdata.overrides = {}
        end

and I noticed, that AddLevelPreInitAny seems to be called twice now, one time with overrides not being nil and a second time after the world was generated with it being nil. Not 100% sure though.


Anyway, when you gave me the code to "prevent gem api to revert my changes", you already said that you did not expect anyone to change generation-settings like this. So maybe this also means and especially meanwhile after this QoL update, that there is a much better way to achieve this? Which is it?

Edited by Serpens
Link to comment
Share on other sites

On 2/22/2021 at 8:47 PM, penguin0616 said:

It's going to be very happy modding :angel:

Especially since "containers" now returns the params table. No more replacement hell for them :>

can you elaborate on this? found nothing abouts this in changelog and no "params" in any container file I searched.

Link to comment
Share on other sites

1 hour ago, penguin0616 said:

It wasn't in the changelog.

image.png.d126d3211128a10bec3e21077e01814e.png

thank you. and can you provide examples which code simnplifies know to what with help of this?

Is it for mod-containers? I currentlyknow 2 ways to add mod containers, while both of them are really too big hassle:
1) Override containers widgetsetup for your specific prefab, return old function for others.
2) Don't override, but pass your params to the widgetsetup call as data. current problem: you need to copy paste the params,if you want the same like already existing -> I guess this is the place where the containers.params kicks in? So now you can do sth like:
inst.components.container:WidgetSetup("shadowBMduelist",containers.params.chester) ? But unfortunately, all those mods which are using the first attempt, always forget to return "data" in their "old_widgetsetup" call, which makes all mods using the second method incompatibel to them =/

 

Edited by Serpens
Link to comment
Share on other sites

@Serpens The new system makes it so you no longer need to override widgetsetup or recursively search for upvalues until you find the params table.

Now to make a container, you can just add it directly to the params table, just like how all the other containers work. 

Example:

-- modmain.lua
local containers = require("containers")

containers.params.another_krampus_sack = containers.params.krampus_sack

containers.params.even_more_krampus_sack =
{
    widget =
    {
        slotpos = {},
        animbank = "ui_krampusbag_2x8",
        animbuild = "ui_krampusbag_2x8",
        pos = Vector3(-5, -120, 0),
    },
    issidewidget = true,
    type = "pack",
    openlimit = 1,
}

for y = 0, 6 do
    table.insert(containers.params.even_more_krampus_sack.widget.slotpos, Vector3(-162, -75 * y + 240, 0))
    table.insert(containers.params.even_more_krampus_sack.widget.slotpos, Vector3(-162 + 75, -75 * y + 240, 0))
end

 

Edited by penguin0616
  • Like 2
Link to comment
Share on other sites

  • Developer
On 3/12/2021 at 6:51 AM, Serpens said:

But what happens afterwards? In your example you added a worldseed option. But how to read out what option the user picked? And how to change eg the worldseed according to the setting?

For world settings, simply create a function in worldsettings_overrides.lua's Pre or Post table(depending on what the setting affects) with the same name as the setting, that function will get called with either "default" or the option selected by the user, for world generation it's more complicated, I would suggest looking at forest_map.lua to see ways to do that.

On 3/12/2021 at 6:51 AM, Serpens said:

I currently use AddLevelPreInitAny to change the tasksetdata directly ("set_pieces", "required_prefabs" and for settings also "overrides"). Is this still the way to go? Or are there better ways now?

This is still the correct way to do this for world generation options.

On 3/13/2021 at 12:52 PM, Serpens said:

All new "world_settings", so the ones you could change after the world was generated, are no longer affected by this code. So no way to change season lenghts, or day length and stuff like this with this code.

On 3/12/2021 at 6:51 AM, Serpens said:

And more important for me: How to change exisiting world settings/creation options, like forcing a specific season and other stuff with my mod?

I would recommend modifying the functions in WorldSettings_Overrides.Pre/Post to either run custom logic, or fake a different setting, like this:

local WSO = require("worldsettings_overrides")
local _day = WSO.Post.day

function WSO.Post.day(difficulty)
	if some_check_for_adventure_mode then
		--force a setting
		difficulty = "onlynight"
	end
	_day(difficulty)
end
--or
function WSO.Post.day(difficulty)
	if some_check_for_adventure_mode then
		--completely custom setting.
		TheWorld:PushEvent("ms_setseasonsegmodifier", day = 0.2, dusk = 1.5, night = 1.3)
		return
	end
	_day(difficulty)
end

 

  • Thanks 2
Link to comment
Share on other sites

On 3/23/2021 at 7:39 PM, zarklord_klei said:

For world settings, simply create a function in worldsettings_overrides.lua's Pre or Post table(depending on what the setting affects) with the same name as the setting, that function will get called with either "default" or the option selected by the user, for world generation it's more complicated, I would suggest looking at forest_map.lua to see ways to do that.

This is still the correct way to do this for world generation options.

I would recommend modifying the functions in WorldSettings_Overrides.Pre/Post to either run custom logic, or fake a different setting, like this:


local WSO = require("worldsettings_overrides")
local _day = WSO.Post.day

function WSO.Post.day(difficulty)
	if some_check_for_adventure_mode then
		--force a setting
		difficulty = "onlynight"
	end
	_day(difficulty)
end
--or
function WSO.Post.day(difficulty)
	if some_check_for_adventure_mode then
		--completely custom setting.
		TheWorld:PushEvent("ms_setseasonsegmodifier", day = 0.2, dusk = 1.5, night = 1.3)
		return
	end
	_day(difficulty)
end

 

thank you. but none of them is a good solution, making it incompatible to other mods that want to change seasons...

My mod should change the season once when starting the world the first time. After that, all other mods should be able to do whatever they want (eg. an item which is able to change season length or whatever).
So I guess I will do a PostInit for the world, then call WorldSettings_Overrides.Post["autumn"]("noseason") or similar (or directly the PushEvent) and try to remember, that I called it once, to not call it again on next loading.
Still no good solution, but better.

edit:
ok, only calling this does not work, because the change is not saved O.ô really mod-unfriendly (because it means an item can not easily change season length or so midgame).
Will see if I can instead override the setting... ... but this also won't help -.- because it seems when loading a game, it will always take the settings chosen in the worldscreen.
All in all I come to the conclusion, that your code is indeed the only viable way to achieve my goal, but it still makes it incompatible to other mods doing the same...

edit2:
Ah no, it would be better if my mod could change the settings on the worldscreen after world was generated. This way the game would save the setting and the users are able to change them, if they don't like them.
But how to do this!?

edit3:
Solution:
because of steam apiv2 I forgot to look at your (Zarklord) worldsettings mod (because I did not find the new location of mods). But I will now do it, like you did it there, simply changing eg. TheWorld.topology.overrides.day and then reload the world once per game. This will change the settings on the worldscreen and if the user is unhappy with it, they can simply change it to whatever they want.


Modding after that QoL update:

Spoiler

All in all the changes of this update regrading modding are horrible, especially APIv2 from steam. I really consider, after so many years, to stop modding DST and abandon my mods...

edit:
Especially because there are no notes about how to deal with many of these changes. Ok, there was never a modding documentation for DST, but when you do so drastic changes, I still expected some kind of introduction and how to deal with it. I'm no expert modder, but I'm a good one and can read/understand the lua game code to find out many things myself. But I really struggled to fix my mods and steam APIv2 makes it even more complicated.

edit2:
don't get me wrong, I like your work Zarklord very much! You are an important addition to the DST dev team and pushing important improvements. And it looks like you are the only dev that is coding and watching the forum at the same time, maybe DST can afford another dev to help in the forum, at least to communicate big game changes and their effect on mods.



 

Edited by Serpens
  • Sad 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...