Jump to content

Recommended Posts

I'm creating this thread as a place to discuss the more advanced topics on:

[*] the Lua language;

[*] the structure of the game;

[*] the possible interplays between the two;

The major motive for its creation is how often the thread [thread=19505]Modders-Your-new-friend-at-Klei![/thread] was giving rise to discussions between forum members themselves, reducing its objectiveness. I intend this to serve as an alternative to that thread, for discussions that do not require input from Ipsquiggle.

I do have a golden rule, though, which I quote from the official Lua website:

"Lua" (pronounced LOO-ah) means "Moon" in Portuguese. As such, it is neither an acronym nor an abbreviation, but a noun. More specifically, "Lua" is a name, the name of the Earth's moon and the name of the language. Like most names, it should be written in lower case with an initial capital, that is, "Lua". Please do not write it as "LUA", which is both ugly and confusing, because then it becomes an acronym with different meanings for different people. So, please, write "Lua" right!

  • Like 1
Link to comment
https://forums.kleientertainment.com/forums/topic/25706-the-lua-centroid/
Share on other sites

Ok, so to begin with, how would you recommend people who are new to Lua get their feet wet with it and start to learn something productive?

Well, the best way to learn how to mod Don't Starve is reading its code. But, of course, to do that effectively you need to first get some general understanding on Lua. How deep into it you want to get is up to you, but the more you learn the easier it will be to understand the game code and how to mod it.There are the great tutorials at lua-users.org, keeping in mind that the game uses Lua 5.1. They should allow you to learn in steps and put that knowledge into practice as you learn. For most modders they should be more than enough, but if you're looking for a broader cover of the language, there's the great textbook Programming in Lua (2nd edition, since that's the one that covers Lua 5.1).Reading mods is also a great method, once you get at least a basic understanding of Lua. And you may take that as your main learning method, as many do, though I personally think some general study on Lua is very important if you want to go beyond the basics.

userdata(I think thats what its called in lua?)[...]actually [MENTION=44092]simplex[/MENTION], you can probably answer something thats been kind of bugging me:Is that what TheSim actually is? userdata or something? Ive got a kind of general idea of what it is and how it works, but not really concrete. Does anyone have a decent list of all of its functionality/methods (just for what they're worth) or anything like that? I havn't been around here long, and I dont use forums that often (so its very like Im just a little dumb here) but I cant the search results to show the actually post containing the result, so searching and just getting back 4 pages of threads isnt very helpful.Or even better, any of the pieces of userdata (Minimap, etc) and any documentation/functions for them

Yes, it's called a userdata. At least until Lua 5.2, where the official term was rebranded user value, mostly to avoid grammatical awkwardness, since Lua had been (though not so consistently) using it as a singular term, but it is plural (the singular would be userdatum). I'll call it userdatum here for the singular version, using userdata for the plural and for the data type. A userdatum is a chunk of memory that gets passed to the engine, which can use it to store whatever it wants, such as a C++ object. The actual contents of a userdatum are not accessible to Lua, i.e. userdata is an opaque type. The way userdata are used in Lua is through their metatables, which should be defined by the engine as a sort of API to deal with them. So usually userdata will have limited usability in Lua if compared to the way they're handled inside the C/C++ code itself. Examples of userdata in the standard Lua library are file handlers.The contents of a userdatum's' metatable are (at least for their core part) defined by the engine itself, since only it can perform the "translations" between how the userdatum is actually implemented and how it may be accessed from Lua. This severely limits how much we, as modders, having access only to the Lua code, can really understand them, since there's no source code for them and they're not documented. So we're limited to seeing how they're used by the standard game code, running some tests and often extrapolating a bit.For the particular case of how Don't Starve uses userdata, you can check its fields by writing something like
for k, v in pairs( getmetatable(TheSim) ) do    print(k)end
If you're wondering why you'd write getmetatable(TheSim) and not getmetatable(TheSim).__index, it's because getmetatable(TheSim) won't actually return the metatable for the userdata, since DS's userdata have a __metatable entry in their real metatable that redirects the output of getmetatable(). What really happens is:
assert(  getmetatable( TheSim ) == debug.getmetatable( TheSim ).__metatable  )assert(  debug.getmetatable( TheSim ).__metatable == debug.getmetatable( TheSim ).__index  )
Edited by simplex
better wording

Well I am hoping to go into higher level programming so you recommend lua-users.org and Programming in Lua as the most effect ways to start learning the basics?

Yes. In fact, if you're planning on really studying Programming in Lua, the tutorials (though great) may not be required at all. The only possible issue is that by learning through a textbook you may feel like you're taking too long to get into the "real stuff". I personally am a textbook guy, with tutorials not really being my style, but to each his own.

After you get a good understanding on Lua, if you want to get into deeper techniques there's the book Lua Programming Gems, which is a collection of articles on the more advanced topics of the Lua language. But it is a quite advanced book, and quite overkill for modding.

---edit---

There is, of course, the Reference Manual, which always comes in handy, but it's not really a primary source for learning Lua unless you already have some understanding in general programming, since it's quite dry. Also, it describes Lua's syntax in Extended Backus–Naur Form.

Edited by simplex

Probably was a good call to start a new thread for this.Ive delt with concepts similar to Lua's metadata in other languages and stuff, so Ive got no problems with how it works and the idea of undefined, bland chunk of memory. What I couldn't understand was how Lua could possibly be calling functions on it, but

The way userdata are used in Lua is through their metatables, which should be defined by the engine as a sort of API to deal with them.

Clears that up nicely. Its also answered my question of how you could get all the defined functions on it (getmetatable, which you explained quite nicely also). I thought it had something to do with the meta table, but I wasnt sure if I quite understood it properly.

Yes, it's called a userdata. At least until Lua 5.2, where the official term was rebranded user value, mostly to avoid grammatical awkwardness, since Lua had been (though not so consistently) using it as a singular term, but it is plural (the singular would be userdatum). I'll call it userdatum here for the singular version, using userdata for the plural and for the data type.

So very beautifully pedantic.cheers Edited by dgweber

Appreciate the help there [MENTION=44092]simplex[/MENTION]. Seems Ipsy has taken on new irresponsibilities that keeps him away from the boards more, so glad to have a new, new friend, haha.

I suppose with Kevin away he's not in a position to dedicate as much time as before to us, modders. And I think too many internal discussions in his thread just make it harder for him to stay objective on the questions actually directed at him when he does go there to reply them. Right now, the last 3 pages of his thread have no posts by him, and having to "catch up" just means less time into dealing with the questions themselves.

So I've hit a small snag with a mod I'm working on.

It involves (only involves, that is a small part of it) a beanstalk that leads to a new worldtype. So far, I've done everything I need to do to the beanstalk to make it 'climbable' and it leads to a new world properly. However, the first issue I need to resolve is forcing it to generate a particular (or from a list) level that is added through modworldgenmain. At first I experimented with forcing an additional cave level, but it being a cave type was particularly problematic for the world generation. So I decided to attempt it with forcing an additional level to the adventure mode playlist, which would've been only reachable through the beanstalk. Unfortunately, the length of the playlist is pretty solid. (Without overriding apparently lots of important things.) So I settled on loading my custom level as a special preset. However, I am having issues making it choose a particular (or from a list) level. This is the relevant code from saveindex.lua:
function SaveIndex:StartSurvivalMode(saveslot, character, customoptions, onsavedcb)	self.current_slot = saveslot--	local data = self:GetModeData(saveslot, "survival")	self.data.slots[self.current_slot].character = character	self.data.slots[self.current_slot].current_mode = "survival"	self.data.slots[self.current_slot].save_id = self:GenerateSaveID(self.current_slot)	self.data.slots[self.current_slot].modes = 	{		survival = {			file = self:GetSaveGameName("survival", self.current_slot),			day = 1,			world = 1,			options = customoptions		},	} 	 	self:Save(onsavedcb)    local starts = Profile:GetValue("starts") or 0    Profile:SetValue("starts", starts+1)    Profile:Save()end
This is the relevant code from beanstalk.lua"
--Makes the beanstalk climbable.local function OnActivate(inst)	--GetPlayer():DoTaskInTime(2, function()		SetHUDPause(true)		local function startadventure()				local function onsaved()		    StartNextInstance({reset_action=RESET_ACTION.LOAD_SLOT, save_slot = SaveGameIndex:GetCurrentSaveSlot()}, true)			--SaveGameIndex.data.slots[self.current_slot].modes.survival = {world = 2}		end				--local levels = require("map/levels")						local level = 0		local slot = SaveGameIndex:GetCurrentSaveSlot()				local customoptions = "SKY_LEVEL_1"		local character = GetPlayer().prefab		SetHUDPause(false)			--SaveGameIndex:FakeAdventure(onsaved, slot, level)			SaveGameIndex:StartSurvivalMode(slot, character, customoptions, onsaved)	end		local function rejectadventure()		SetHUDPause(false) 		inst.components.activatable.inactive = true		ProfileStatsSet("portal_rejected", true)	end				local options = {		{text="YES", cb = startadventure},		{text="NO", cb = rejectadventure},  	}	TheFrontEnd:PushScreen(PopupDialogScreen(		"Up and Away", 	"The land above is strange and foreign. Do you want to continue?",		options))		--end)	end
Is there something relevant in saveindex.lua or something that I may be missing somehow?

[MENTION=8791]debugman18[/MENTION]

I really have no experience on this, so I feel I may be of limited help.

But I can say setting customoptions to the preset name is the wrong way to do it. To see how it should be done, check screens/newgamescreen.lua (functions NewGameScreen:Customize() and NewGameScreen:Start()) for the context. Then check screens/customizationscreen.lua (look at how the CustomizationScreen constructor defines the entries of self.presets), and finally see how they're used in CustomizationScreen:LoadPreset(). If I got the details correctly (which I may as well have not, since I just glimpsed through the code), you should define customoptions the following way, assuming "SKY_LEVEL_1" is your level id:

local customoptions = {    preset = "SKY_LEVEL_1",}
Edited by simplex

[MENTION=8791]debugman18[/MENTION]

I really have no experience on this, so I feel I may be of limited help.

But I can say setting customoptions to the preset name is the wrong way to do it. To see how it should be done, check screens/newgamescreen.lua (functions NewGameScreen:Customize() and NewGameScreen:Start()) for the context. Then check screens/customizationscreen.lua (look at how the CustomizationScreen constructor defines the entries of self.presets), and finally see how they're used in CustomizationScreen:LoadPreset(). If I got the details correctly (which I may as well have not, since I just glimpsed through the code), you should define customoptions the following way, assuming "SKY_LEVEL_1" is your level id:

local customoptions = {    preset = "SKY_LEVEL_1",}
I actually figured out how to make it generate my preset through the StartFakeAdventure(). :D

(That bit with options didn't work though. Apparently it's a little more complicated.)

Now to transform everything generated naturally into different prefabs...

[MENTION=44092]simplex[/MENTION], back on Test Tools previews. Have any idea why 'slurpers' will not show any anim in preview?

If you remember, many anims show using my_anim:GetAnimState():PlayAnimation("idle")

those that don't are handled through the builds.lua file or manually: build_info.slurper.baseanim = "idle_loop"

slurper (and maybe a couple others) don't show no matter what. I've put every single PlayAnimation variation in from the SGslurper file and nothing.

Thinking that maybe you had to be in a cave to get it to show (really grasping here!) I dug down and tried it in a cave, but nope.

Wondering if some game variable has to be in place first because I can't spawn a slurper either...

Any thoughts?

Edited by tehMugwump

[MENTION=44092]simplex[/MENTION], back on Test Tools previews. Have any idea why 'slurpers' will not show any anim in preview?

If you remember, many anims show using my_anim:GetAnimState():PlayAnimation("idle")

those that don't are handled through the builds.lua file or manually: build_info.slurper.baseanim = "idle_loop"

slurper (and maybe a couple others) don't show no matter what. I've put every single PlayAnimation variation in from the SGslurper file and nothing.

Thinking that maybe you had to be in a cave to get it to show (really grasping here!) I dug down and tried it in a cave, but nope.

Wondering if some game variable has to be in place first because I can't spawn a slurper either...

Any thoughts?

Well, according to the sluper.lua prefab file, the basic animation is really "idle_loop". However, the PlayAnimation() call receives a second argument, true. That may be relevant. But since PlayAnimation() is defined at the engine level, I can't really know what the second argument is for, except by asking Ipsquiggle.

Well, according to the sluper.lua prefab file, the basic animation is really "idle_loop". However, the PlayAnimation() call receives a second argument, true. That may be relevant. But since PlayAnimation() is defined at the engine level, I can't really know what the second argument is for, except by asking Ipsquiggle.

Well considering some other entities (like the tentapillar) use things like
inst.AnimState:PlayAnimation("idle","loop")

and others use

inst.AnimState:PlayAnimation("idle_loop",true)
the second argument seems to be setting the behavior of the animation(?).

Try using "loop" or true for the second argument [MENTION=10028]tehMugwump[/MENTION]

Well considering some other entities (like the tentapillar) use things like

inst.AnimState:PlayAnimation("idle","loop")

and others use

inst.AnimState:PlayAnimation("idle_loop",true)
the second argument seems to be setting the behavior of the animation(?).

Try using "loop" or true for the second argument [MENTION=10028]tehMugwump[/MENTION]

Yes, they may be identifying a "subanimation". I wish those things had some documentation :/

Most of the code can just be read. But things like that, directly exported by the engine, are a different story.

Yeah, first thing I tried was adding true, but using our builds.lua system it ignores it or errors out whether I add

build_info.slurper.baseanim = "idle", true

or

build_info.slurper.baseanim = ("idle", true)

Seriously, I just wish there was some consistency here, but it's all over the map...

Well considering some other entities (like the tentapillar) use things like

inst.AnimState:PlayAnimation("idle","loop")

and others use

inst.AnimState:PlayAnimation("idle_loop",true)
the second argument seems to be setting the behavior of the animation(?).

Try using "loop" or true for the second argument [MENTION=10028]tehMugwump[/MENTION]

Yes, they may be identifying a "subanimation". I wish those things had some documentation :/

Most of the code can just be read. But things like that, directly exported by the engine, are a different story.

Yeah, first thing I tried was adding true, but using our builds.lua system it ignores it or errors out whether I addbuild_info.slurper.baseanim = "idle", true orbuild_info.slurper.baseanim = ("idle", true) Seriously, I just wish there was some consistency here, but it's all over the map...

But does simply passing true as a second argument fix it? Because if so, I can just extend builds.lua to account for more than one argument.

Quick question to whomever can answer it: In several mods (RPG Hud off the top of my mind) have a folder called "newfunctions" that has a AddWidgetPostInit which someone added in. I was just curious who wrote it in the first place (since there's no documentation or credits or anything)!edit: I was wrong, there were credits in the main mod file for it: No_Signal if anyone cared for any random reason...

Edited by dgweber

Quick question to whomever can answer it: In several mods (RPG Hud off the top of my mind) have a folder called "newfunctions" that has a AddWidgetPostInit which someone added in. I was just curious who wrote it in the first place (since there's no documentation or credits or anything)!edit: I was wrong, there were credits in the main mod file for it: No_Signal if anyone cared for any random reason...

Yes, you answered your own question before I could. :wink:An "official" version of AddWidgetPostInit (or more precisely, of postinits for HUD elements) [thread=24358]is planned[/thread]. But the one in current use is by [MENTION=26201]no_signal[/MENTION].

So I'm trying to 'cheat' the world generation into replacing natural world objects with my own prefabs.

local function evergreens_ret(inst)	inst:AddTag"evergreen_ret"endlocal function onUp(inst)                SetHUDPause(true)		--Define the new world objects we are placing.		local sunflower = SpawnPrefab("sunflower")		--Define the old world objects we are replacing.		local evergreens_ret = TheSim:FindFirstEntityWithTag("evergreen_ret")				--Make sure that the player climbed the beanstalk.                if GetPlayer().goneUp == true and GetPlayer().goneUpRemember == false then						--Place the new world objects.						sunflower.Transform:SetPosition(evergreens_ret.Transform:GetWorldPosition())						--Remove the old world objects.			evergreens_ret:Remove()		end	                SetHUDPause(false)end
This is the function that sends the player to that particular world.
--Makes the beanstalk climbable.local function OnActivate(inst)	--GetPlayer():DoTaskInTime(2, function()		SetHUDPause(true)		local function startadventure()				local function onsaved()		    StartNextInstance({reset_action=RESET_ACTION.LOAD_SLOT, save_slot = SaveGameIndex:GetCurrentSaveSlot()}, true)			SaveGameIndex.data.slots[self.current_slot].modes.adventure = {world = 1}		end				--local levels = require("map/levels")						local level = 8		local slot = SaveGameIndex:GetCurrentSaveSlot()				local customoptions = {			preset = {"SKY_LEVEL_1"},		}				--local options = CustomizationScreen.options		local character = GetPlayer().prefab				SetHUDPause(false)					--options = {		--	preset = "SKY_LEVEL_1",		--}				--CustomizationScreen:LoadPreset("SKY_LEVEL_1")		GetPlayer().goneUp = true		GetPlayer().goneUpRemember = false					SaveGameIndex:FakeAdventure(onsaved, slot, level)					--SaveGameIndex:StartSurvivalMode(slot, character, customoptions, onsaved)	end		local function rejectadventure()		SetHUDPause(false) 		inst.components.activatable.inactive = true		ProfileStatsSet("portal_rejected", true)	end				local options = {		{text="YES", cb = startadventure},		{text="NO", cb = rejectadventure},  	}	TheFrontEnd:PushScreen(PopupDialogScreen(		"Up and Away", 	"The land above is strange and foreign. Do you want to continue?",		options))		--end)	end
Any idea how I could have the onUp() function run (until there are no longer evergreens, in this particular case) only once, when the player enters the world for the first time?I was thinking of using DoPeriodicTask(), but I'm not sure what to attach it to. Perhaps I could attach it through my AddPrefabPostInit("evergreen", evergreens_ret)? I imagine that would cause quite some lag, though.A note: I could probably easily do a lot of the things I'm trying to by overwriting game prefabs with my own, but I'm trying to make the mod as sturdy as I can. I'd like other mods to be compatible, as well, but that's not nearly as much as an issue as making sure it doesn't break on each update. Edited by debugman18

So I'm trying to 'cheat' the world generation into replacing natural world objects with my own prefabs.

local function evergreens_ret(inst)	inst:AddTag"evergreen_ret"endlocal function onUp(inst)                SetHUDPause(true)		--Define the new world objects we are placing.		local sunflower = SpawnPrefab("sunflower")		--Define the old world objects we are replacing.		local evergreens_ret = TheSim:FindFirstEntityWithTag("evergreen_ret")				--Make sure that the player climbed the beanstalk.                if GetPlayer().goneUp == true and GetPlayer().goneUpRemember == false then						--Place the new world objects.						sunflower.Transform:SetPosition(evergreens_ret.Transform:GetWorldPosition())						--Remove the old world objects.			evergreens_ret:Remove()		end	                SetHUDPause(false)end
This is the function that sends the player to that particular world.
--Makes the beanstalk climbable.local function OnActivate(inst)	--GetPlayer():DoTaskInTime(2, function()		SetHUDPause(true)		local function startadventure()				local function onsaved()		    StartNextInstance({reset_action=RESET_ACTION.LOAD_SLOT, save_slot = SaveGameIndex:GetCurrentSaveSlot()}, true)			SaveGameIndex.data.slots[self.current_slot].modes.adventure = {world = 1}		end				--local levels = require("map/levels")						local level = 8		local slot = SaveGameIndex:GetCurrentSaveSlot()				local customoptions = {			preset = {"SKY_LEVEL_1"},		}				--local options = CustomizationScreen.options		local character = GetPlayer().prefab				SetHUDPause(false)					--options = {		--	preset = "SKY_LEVEL_1",		--}				--CustomizationScreen:LoadPreset("SKY_LEVEL_1")		GetPlayer().goneUp = true		GetPlayer().goneUpRemember = false					SaveGameIndex:FakeAdventure(onsaved, slot, level)					--SaveGameIndex:StartSurvivalMode(slot, character, customoptions, onsaved)	end		local function rejectadventure()		SetHUDPause(false) 		inst.components.activatable.inactive = true		ProfileStatsSet("portal_rejected", true)	end				local options = {		{text="YES", cb = startadventure},		{text="NO", cb = rejectadventure},  	}	TheFrontEnd:PushScreen(PopupDialogScreen(		"Up and Away", 	"The land above is strange and foreign. Do you want to continue?",		options))		--end)	end
Any idea how I could have the onUp() function run (until there are no longer evergreens, in this particular case) only once, when the player enters the world for the first time?I was thinking of using DoPeriodicTask(), but I'm not sure what to attach it to. Perhaps I could attach it through my AddPrefabPostInit("evergreen", evergreens_ret)? I imagine that would cause quite some lag, though.A note: I could probably easily do a lot of the things I'm trying to by overwriting game prefabs with my own, but I'm trying to make the mod as sturdy as I can. I'd like other mods to be compatible, as well, but that's not nearly as much as an issue as making sure it doesn't break on each update.Well, assuming you don't want to do that at the world generation level, I think postinits are the best way. Otherwise, you could patch the player's OnSave/OnLoad to store a flag (or create a dedicated prefab/component that just runs this function and stores the flag).

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
×
  • Create New...