Jump to content

Retaining a variable when entering a Cave


Recommended Posts

I've found a curious quirk in the gameplay logic and am wondering the best way to work around it

 

I've got a few variables floating around modmain.lua, which operate and are called just fine when playing. Let's say like this:

example = Class(function(self, inst)    self.powerLevel = 9000    self.cake = falseend)

As the game progresses, these get updated (which is important)

 

However, the challenge is that I'd like these variables to be carried over when a player enters a cave (as, for all intents and purposes, it's continuing the same game)

 

The problem I've found though is that caves are treated as a new game in itself (it loads/saves a completely different save file) - As such, all of my variables get reset when I enter one

 

Is there a way I can carry them over? My only guess is that I have to attach them to the player (whose data gets transferred between the 2) - If so, how do I go about this? Both to save the variables and retrieve them later on

Link to comment
Share on other sites

Good to know. Thank you Mobbstar

 

Is there a particular convention or is

self.data.slots[self.current_slot].modes.survival.playerdata.NEW_VARIABLE = example

... pretty much the way to do it? (refactoring 'self' to an external call of 'SaveIndex' of course) Or are we looking at the AddPlayerPostInit() method?

Link to comment
Share on other sites

Note that this takes a bit more work if you wish the variables to NOT persist over sessions, and only persist when traveling between islands.

 

 

Create a custom component, e.g. "moddata".

 

 

scripts/components/moddata.lua:

local ModData = Class(function(self, inst) 	self.inst = inst  self.data = {}end)function ModData:SetData(data)    self.data = dataendfunction ModData:GetData()    return self.dataendfunction ModData:OnSave()    return { data = self.data }endfunction ModData:OnLoad(saved)    self.data = saved.data or {}end return ModData

Note-- getters/setters aren't "mandatory", and naturally you can use more than just a "data" property.

 

 

modmain.lua:

AddPlayerPostInit(function(inst) inst:AddComponent("moddata") end)

(or add the component to your existing AddPlayerPostInit)

Link to comment
Share on other sites

Wonderful - Thank you Corrosive

 

Luckily I have the onSave and onLoad functions already in place. So it's just sticking it onto the player

 

I already have an AddPlayerPostInit function, however. Adding a previously-defined function:

AddPlayerPostInit(function1)

How would I go about adding a second function to this? I know LUA has the capacity to support lists for args, so would something like:

AddPlayerPostInit(function1, function(inst) inst:AddComponent("exmaple") end)

... work? Can AddPlayerPostInit support multiple calls, or will the later ones overwrite the first?

 

I considered trying to amend the logic of the first function... but it doesn't include inst as an arg, so I'm wary of inadvertently interfering with its logic (plus it'd really be bad coding to shoehorn unrelated stuff within another function)

Link to comment
Share on other sites

Take a look at the AddPrefabPostInitAny definition, which is what AddPlayerPostInit uses (it just filters out any non-player prefab before passing the prefab to AddPrefabPostInitAny): 

	env.AddPrefabPostInitAny = function(fn)		initprint("AddPrefabPostInitAny")		table.insert(env.postinitfns.PrefabPostInitAny, fn)	end

As you can see, it expects exactly one argument, and for that argument to be a function.  Any additional argument would just get ignored.

 

You technically could just call AddPlayerPostInit twice, but there's no real reason to. Calling it once allows you to "hook" into the player prefab, so you can do anything you wish to do with it from that one function.  You can write your code a number of ways, depending on how you want to organize it.  For example:

local function function1  ...your code...  inst:AddComponent("example")endAddPlayerPostInit(function1)

or if you really wanted to segregate the code:

local function function1(inst)...your code...endlocal function function2(inst)  inst:AddComponent("exmaple")endlocal function playerpostinitfn(inst)  function1(inst)  function2(inst)end

or a more compact version:

local function function1(inst)  ...your code...endlocal function function2(inst)  inst:AddComponent("exmaple")endAddPlayerPostInit(function(inst) function1(inst) function2(inst) end)
Link to comment
Share on other sites

Sure Corrosive:

 

The code's been leveraged off the 'Compromising Survival' mod (the event listener on death and a question pop-up was exactly what I needed)

local function tweak_death_eventlistening(player)    player:ListenForEvent("death", handle_death)    player.ListenForEvent = (function()        local ListenForEvent_base = player.ListenForEvent        return function(self, ev, fn, source)            if ev == "death" and (not source or source == self) then                return Outcomes.AddDyingCallback(fn)            else                return ListenForEvent_base(self, ev, fn, source)            end        end    end)()end

Which of course leads to:

AddPlayerPostInit(tweak_death_eventlistening)

It's just dawned on me though... My initial thought is that inst was missing as an arg in tweak_death_eventlistening (so I couldn't easily add it in). But thinking about it... considering how the method is used (and that it's being added to the player) - inst and player are the same thing - It's just the author of this one chose 'player' as a more meaningful name

 

It just clicked it doesn't matter what variable name you pass in is, the arg name can be whatever it damn-well likes

 

After all, though completely meaningless in terms of var names, so long as all of the variables being thrown around below are, say, strings - this would work

local function testA(iceCreamVan)    print(iceCreamVan)endlocal function testB(chinchilla, aubergine, those_annoying_fragments_of_paper_after_cutting)    testA(chinchilla)    testA(aubergine)    testA(those_annoying_fragments_of_paper_after_cutting)end

So really, as you alluded to. I just augment the method with Player:AddComponent("example")?

 

So last point for it: once it's hooked onto the player, what's the best way of getting it back? I'm guessing we start with GLOBAL.getPlayer() and it's located on that somewhere?

Link to comment
Share on other sites

inst and player are the same thing

 

That's why I asked, I thought that might have been the case ;)

 

And yeah, GLOBAL.GetPlayer() will always return the player.  The player prefab is a singleton-- that is-- there is only ever one.  TheSim and TheInput are other examples of singletons(I imagine that's why they used the prefix "The" for them.  In DST you can use ThePlayer instead of GetPlayer(), but not in DS.  Though in your modmain, you can always do local ThePlayer = GetPlayer() to emulate that.

 

 

located on that somewhere?

 

Not on that, it's effectively the same thing.  You can verify from your PlayerPostInit function:

 

print("inst = GetPlayer()?  " .. inst == GLOBAL.GetPlayer())

Link to comment
Share on other sites

Not on that, it's effectively the same thing

 

My apologies -  I mean getting the variable itself I save to the player

 

Getting the player is easy enough, it's just figuring out where on the player the variable will be saved (the one downside with LUA's ability to just stick on variables - hard to know where it's saving them unless you can track down the setter methods)

 

Based upon a scout of the code... I'm guessing something like:

GLOBAL.GetPlayer().components["example"]
Link to comment
Share on other sites

@silktie That is correct. Components are added to the components table in the player's instance (which is also a table).

player_inst = GLOBAL.GetPlayer()-- You can also write it like this:player_inst.components.example

-

 

The AddComponent function is located in entityscript.lua

function EntityScript:AddComponent(name)    if self.components[name] then        print("component "..name.." already exists!")    end    local cmp = LoadComponent(name)    assert(cmp, "component ".. name .. " does not exist!")        local loadedcmp = cmp(self)    self.components[name] = loadedcmp    local postinitfns = ModManager:GetPostInitFns("ComponentPostInit", name)    for k,fn in ipairs(postinitfns) do        fn(loadedcmp,self)    endend
Link to comment
Share on other sites

I'm sorry to more or less necro this topic, but I am having a save/load issue, and thought it'd be fitting to post here than in a new topic.

 

I am trying to save (and load, obviously) some arbitrary data, together with entity references (based on objectspawner component). However, the method explained here doesn't work for me, the LoadPostPass doesn't fire. Here's the component in question, if you want to take a look:

--This component is attached to the player and tracks all entities that have effects with duration
 

local Cmp = Class(function(self, inst)
    self.anchor = inst
--player
    self.affected = {} --affected[guid_of_inst][number] = Effect Class
end)

function Cmp:Track(inst,eff)
--adds an effect to the list
    print(VERBOSITY.DEBUG,"TRACKING",eff,inst)
    if not self.affected[inst.GUID] then
        self.affected[inst.GUID] = {}
    end
    table.insert(self.affected[inst.GUID],eff)
end

function Cmp:OnSave(data)
    if DumpEffects then return end
    local data = {}
    local references = {}
    for guid,fx in pairs(self.affected) do
        print(VERBOSITY.DEBUG,"SAVING FOR",Ents[guid])
        data[guid] = {}
        for _,eff in pairs(fx) do
            print(VERBOSITY.DEBUG,"SAVING EFFECT",eff.name)
            table.insert(data[guid],eff:GetSaveData())
        end
    
        if guid ~= self.anchor.GUID then
--saving when the player had an effect crashed the game, idk about other entities
            print(VERBOSITY.DEBUG,"REFERENCING",guid)
            table.insert(references,guid)
        end
    end
    
    if #references == 0 then references = nil end
    if #data == 0 then data = nil end
    print(VERBOSITY.DEBUG,"DONE WRITING SAVE DATA, RETURNING")

    return data, references
end

function Cmp:LoadPostPass(newents,data)
    if data and not DumpEffects then
        for guid,fx in pairs(data) do
            if newents[guid] then
            print(VERBOSITY.DEBUG,"LOADING DATA FOR",newents[guid])
                self.affected[guid] = {}
                for _,v in pairs(fx) do
                    eff = POTIONEFFECTBASIC():UseData(v)
                    table.insert(self.affected[guid],eff)
                    eff:drinkfn(newents[guid].entity)
                end
            else
                Print(VERBOSITY.WARNING,"FAILED TO FIND SAVED ENTITY",guid,"EFFECT #1:",fx[1].name)
            end
        end
    end
end

I've considered using one of the two following methods instead:

  1. This method (already used by debugman's "feats" mod), and then test for the save slot
  2. Make a persistent dummy prefab with the save and load functions, and spawn it only once per world

 

Am I doing something wrong with the PostInit method? Is there any reason I should use the former alternative rather than the latter? If I get no replies, I'll just make a dummy. I like me some dummies.

Link to comment
Share on other sites

@Mobbstar Have you seen/tried this?

Nevermind, ignore that!

 

Edit: No wait, don't ignore that. Why exactly is that crossed out? Did it not work?

 

Edit 2: If you were referring to that as the former alternative (as opposed to the OnSave things mentioned previously in the thread) then.. I can't say how it will be affected performance wise, but it just seems to make more sense than having an empty prefab that doesn't do anything else in the game.

If you don't want to make a new file for your data, you can even use one of the existing files. Just be sure to check which keys are empty.

Link to comment
Share on other sites

@Mobbstar Have you seen/tried this?

Nevermind, ignore that!

 

Edit: No wait, don't ignore that. Why exactly is that crossed out? Did it not work?

 

Edit 2: If you were referring to that as the former alternative (as opposed to the OnSave things mentioned previously in the thread) then.. I can't say how it will be affected performance wise, but it just seems to make more sense than having an empty prefab that doesn't do anything else in the game.

If you don't want to make a new file for your data, you can even use one of the existing files. Just be sure to check which keys are empty.

 

I've tried the dummy prefab, and the LoadPostPass works now (I think), but since saving crashes the game, it's pointless anyways. See for yourself.

Link to comment
Share on other sites

disregard, I need to test further

 

 

Yeah, you're going to have some problems with saving the drinkfns on the potions like that, I think.

 

Since by the time they get stored as values they're compiled into bytecode, if you try to retrieve them in a new execution context, they aren't going to be able to find upvalues or other stale references.   You might need to rebuild each drinkfn on load using saved data.

Link to comment
Share on other sites

@Corrosive Thank you for pointing that out. It persuaded me to research more on how upvalues work.

if anyone else is interested, here's a long explanation which I found quite helpful.

 

As for SpawnPrefab, wouldn't the reference to the upvalue be stored within the function's own scope? What do you mean by "longer than the original context of SpawnPrefab", is it being changed or removed anywhere?

 

Edit: Just saw your edit. You probably should've left the original post too so people know what you're referring to, haha.

Link to comment
Share on other sites

@Blueberrys,

 

<.< ...  >.>

 

I may or may not have copied the original post to a notepad then closed the notepad thinking I wouldn't need to repost it.

 

 

Anyhoo, yeah the reference to the upvalue (or other referenced value) is stored within the scope, but once the actual data the reference to the upvalue is referencing goes away, that reference becomes stale.  I'm not sure what kind of error Lua would trigger, but I'm near positive that it should trigger one.

 

It's like retaining an address book between tearing down and rebuilding a city.  You still have the names and addresses, but chances are the same people aren't living at those addresses.

 

 

Then again I may be wrong about how the game serializes the data-- I'll have to take a closer look and/or do some testing.

Link to comment
Share on other sites

@Corrosive Hmm. But I don't see any reason for SpawnPrefab's original data to be discarded at any time while the game world is still running. I think it should be fine as long as the code doesn't execute outside of the game world.

IIRC The OnSave/Load methods on potions were storing/retrieving fx objects (that contain fx.drinkfn or something to that effect. I think what would be preferable is just to have a registry containing each type of potion effect, so that potions could store a table containing the list of effects they confer & any variables needed to pass to that effect's function (like a slightly more advanced version of the ACTIONS registry).

(Writing this on my phone atm but I'll check to make sure when I get back to my comp.)

Link to comment
Share on other sites

Hmm. But I don't see any reason for SpawnPrefab's original data to be discarded at any time while the game world is still running. I think it should be fine as long as the code doesn't execute outside of the game world.

 

Dumb question, but is this all about my mod or another issue? nvm it is. I've thought about what you stated, and it's probably a lot more stable and efficient to store all of the ongoing effects in Profile, if only I would build up the courage and free time to mess with those systems. Maybe tomorrow :razz:

 

And yes, @Corrosive, I've considered to use string.dump on the functions for saving, but for some reason it seems to work without. That's how I roll: When it works, it's fine.

 

Also, on a scale from 1 to 10, how aching is the fact that I construct a new class for every effect on every instance? (stacks act as one instance, so it's suprisingly tame so far) EDIT: Why the heck did I do that?! There's only the data and three functions on the standart Effect class, all three of which could also be declared in the potion, or even better: modmain (aka once). Gosh I'll need to rewrite half of the code I think.

Link to comment
Share on other sites

Dumb question, but is this all about my mod or another issue?

 It's about your mod, but not the issue you mentioned. Corrosive found something else in your mod and we started talking about that, but he edited the original post now.

 

I've thought about what you stated, and it's probably a lot more stable and efficient to store all of the ongoing effects in Profile, if only I would build up the courage and free time to mess with those systems. Maybe tomorrow

Er, again, have you tried using that module? xD It's fairly straight forward, should only take you about 5 minutes to figure it out. Or are you talking about the second issue, too?

 

Also, on a scale from 1 to 10, how aching is the fact that I construct a new class for every effect on every instance? (stacks act as one instance, so it's suprisingly tame so far)

That doesn't sound right.. I think I'll have a look at your code. >->

If 1 is "unconcerned" and 10 is "Noo whyyyyyyyy" then probably 9.

Link to comment
Share on other sites

@Mobbstar,
 
I considered string.dump too, but although I had never had to use it for anything before, I did notice while screwing around that it output an unreadable string, so I assumed that this was also binary bytecode.  After checking with the documentation(just now), it says:


Returns a string containing a binary representation of the given function, so that a later loadstring on this string returns a copy of the function. function must be a Lua function without upvalues.

 
Them pesky upvalues :(
 
@Mobbstar, @Blueberrys,
I had a little bit of time to test just now, and I'm next to positive that what I described is happening.  You should be able to easily recreate this test I did:
 
Here's the pertinent section of modmain.lua for reference:


 
POTIONEFFECTS.Light = Class(POTIONEFFECTBASIC,function(self, str, val, altfx, anti)	POTIONEFFECTBASIC._ctor(self,str,"light",altfx)		if val == 0 then	Print(VERBOSITY.WARNING,"WARNING, used Light effect with value 0. That can confuse other effects!")	end		self.args = {val = val}	function self:drinkfn(doer)		if doer then			if not doer.wormlight then				local light = _G.SpawnPrefab("wormlight_light")				light.components.spell:SetTarget(doer)				if not light.components.spell.target then					light:Remove()				end				light.components.spell:StartSpell()			end			doer.wormlight.components.spell.duration = val			doer.wormlight.components.spell:ResumeSpell()		end	end	self.solutions = antiend)



Part 1:
Create two potions (specifically, a wormlight and an empty alchemypotion).  Drink 1 to verify that it works.  Save and quit game.  Reload the save.  Now when you try to drink the remaining potion, you'll get this error:

ZTXtqlnl.jpg
Here's that line:
local light = SpawnPrefab("wormlight_light")

(SpawnPrefab is imported as a local in modmain's context at the top of the file, outside of the drinkfn)
 
 
Part 2:
 
Edit line 514 of modmain.lua to be the following:

local light = _G.SpawnPrefab("wormlight_light")

 
Reload the save.  Discard the potion from Part 1.  Since its stale references persist, it'll always cause an error.  Create a new potion using a wormlight and an alchemypotion.  Save and quit game.  Reload the save. Try drinking potion.  You'll get this new error:
DlJeXnPl.jpg
 
Well that got us past the SpawnPrefab hurdle, but unfortunately it looks like there's another stale reference (or some other error) once it it gets to line 522 and calls ResumeSpell().  I haven't tested this, though it would be pretty easy to... but my guess is that it's caused by the preceding statement at 521:

	doer.wormlight.components.spell.duration = val        doer.wormlight.components.spell:ResumeSpell()

val is another upvalue, this time coming from the class constructor.  I imagine(but haven't tested) that since the upvalue is on the right side of an assignment operator, nil is getting assigned to duration.  Later when I have some time I'll check if the error still occurs when assigning a numeric literal to duration.
 
Part 3: (just to confirm that the functions really are getting stored as compiled bytecode)
 
Modify the code of the drinkfn so that it's guaranteed to raise an error before any other problems arise.  Ex:


    function self:drinkfn(doer)        assert(false, "YOU DONE MESSED UP NOW")          if doer then            if not doer.wormlight then...

Load game.  Brew one last wormlight potion. Save and quit.  Before loading the saved game, modify the intentionally erroring code to something innocuous.  Blank spaces work just fine:

    function self:drinkfn(doer)        if doer then            if not doer.wormlight then...

Load saved game.  Drink potion.  Error as expected:
pqFfwMil.jpg

 

 

Edit:

 

Also, on a scale from 1 to 10, how aching is the fact that I construct a new class for every effect on every instance?

 

If it was a strongly typed language, 9.2.  Since it's Lua... ehhhhh... I'm gonna go with 7.5.  It's still a very Rube-Goldbergian way of going about it, lol.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

Please be aware that the content of this thread may be outdated and no longer applicable.

×
  • Create New...