Jump to content

Need help understanding saving items - creating a "bottomless" chest


Recommended Posts

Hi all! I'm currently trying to create a sort of "clown car" chest, meaning a chest that can store more items than initially expected. To do this, the chest iterates over every item in the container once it is closed, and rolls a chance for that item to disappear from the main chest storage and instead get saved in a hidden "vault" that players cannot access. The next time the chest is opened, any empty slots have a chance to be filled in with a random item from the chest's hidden vault. The resulting effect is that items sort of just disappear and reappear at random times, often leaving empty spaces to stuff more items in.

Currently, all of that is working fine. What I'm having trouble with is saving that hidden vault so it's loaded in the next session. I made an attempt at it, but I'm just taking ineffective shots in the dark here. Hopefully someone more familiar with the game's data structures will be able to help. I tried saving the vault as-is, and saving the save records in a table, but neither seems to work properly.

Thanks guys! I've included the relevant code snippets below.

OnOpen

local function onopen (inst)
	local opener = inst.components.container.opener

	-- Require sanity tribute and set perishable multiplier back to normal
	-- Chance to restore items from vault
	if opener.components.sanity and not opener.components.sanity:IsCrazy() then
		opener.components.sanity:DoDelta(-TUNING.SANITY_SMALL)
		
		local inv = inst.components.container 
		for i = 1, inv:GetNumSlots() do
			local item = inv:GetItemInSlot(i)
			if item then
				if item.components.perishable then
					item.components.perishable:SetLocalMultiplier(1)
				end
			elseif #inst.vault > 0 then
				if math.random() < SHUFFLECHANCE then
					local vaultindex = math.random(1,#inst.vault)
					local founditem = table.remove(inst.vault, vaultindex)
					if founditem.components.perishable then
						founditem.components.perishable:StartPerishing()
					end
					inv:GiveItem(founditem, i)
				end
			end
		end	
		
	-- If insufficient sanity, prevent from opening
	else
		opener.components.talker:Say("Huh? Th-there's a box here right?")
		inst.components.container:Close()
		return
	end
    
  	-- [additional code redacted for brevity]
  	-- [basically a chance to spawn a few random items on open]
end

 

OnClose

local function onclose(inst)
	if not inst:HasTag("burnt") then
		inst.AnimState:PlayAnimation("close")
		inst.AnimState:PushAnimation("closed", false)
		inst.SoundEmitter:PlaySound("dontstarve/wilson/chest_close")
		
		local inv = inst.components.container 
		for i = 1, inv:GetNumSlots() do
			local item = inv:GetItemInSlot(i)
			
			if item then
				-- Chance to disappear into chest vault
				if not item:HasTag("irreplaceable") and math.random() < SHUFFLECHANCE then
					if item.components.perishable then
						item.components.perishable:StopPerishing()
					end
					table.insert (inst.vault, inv:RemoveItemBySlot(i))
				elseif item.components.perishable then
					item.components.perishable:SetLocalMultiplier(0.25)
				end
			end
		end	
	end
end

 

Non-functional OnSave and OnLoad

local function onsave(inst, data)
    if inst.components.burnable ~= nil and inst.components.burnable:IsBurning() or inst:HasTag("burnt") then
        data.burnt = true
    end
	
	data.vault = {}
    for k,v in ipairs(inst.vault) do
        if v:IsValid() and v.persists then --only save the valid items
			table.insert(data.vault, v:GetSaveRecord())
        end
    end
end

local function onload(inst, data)
    if data ~= nil and data.burnt and inst.components.burnable ~= nil then
        inst.components.burnable.onburnt(inst)
    end
	
	if data and data.vault then
		for k,v in ipairs(data.vault) do
            local item = SpawnSaveRecord(v)
            if item then
				table.insert(inst.vault, item)
            end
        end
	end
end

Main Function

local function fn()
	local inst = CreateEntity()

	inst.entity:AddTransform()
	inst.entity:AddAnimState()
	inst.entity:AddSoundEmitter()
	inst.entity:AddLight()
	inst.entity:AddMiniMapEntity()
	inst.entity:AddNetwork()

	inst.MiniMapEntity:SetIcon("pandoraschest.png")

	inst:AddTag("structure")
	inst:AddTag("chest")
	inst:AddTag("fridge")
	
	inst.Light:SetFalloff(1)
	inst.Light:SetIntensity(0.5)
	inst.Light:SetRadius(2)
    inst.Light:SetColour(180/255, 195/255, 150/255)
	inst.Light:Enable(true)
	inst.Light:EnableClientModulation(true)
	
	inst._fadeval = net_float(inst.GUID, "fireflies._fadeval")
    inst._faderate = net_smallbyte(inst.GUID, "fireflies._faderate", "onfaderatedirty")
    inst._fadetask = nil

	inst.AnimState:SetBank("pandoras_chest")
	inst.AnimState:SetBuild("pandoras_chest")
	inst.AnimState:PlayAnimation("closed")

	MakeSnowCoveredPristine(inst)

	inst.entity:SetPristine()

	if not TheWorld.ismastersim then
        inst:ListenForEvent("onfaderatedirty", OnFadeRateDirty)
		inst:DoTaskInTime(0.1, function(inst)
			inst.replica.container:WidgetSetup("shadowchester")
		end)
		return inst
	end
	
	inst:AddComponent("playerprox")
    inst.components.playerprox:SetOnPlayerNear(onplayerprox)
	inst.components.playerprox:SetOnPlayerFar(fadeout)
    inst.components.playerprox:SetDist(3,4)

	inst:AddComponent("inspectable")
	inst:AddComponent("container")
	inst.components.container:WidgetSetup("shadowchester")
	inst.components.container.onopenfn = onopen
	inst.components.container.onclosefn = onclose
	
	inst:AddComponent("sanityaura")
	inst.components.sanityaura.aura = -TUNING.SANITYAURA_TINY

	inst:AddComponent("lootdropper")
	inst:AddComponent("workable")
	inst.components.workable:SetWorkAction(ACTIONS.HAMMER)
	inst.components.workable:SetWorkLeft(5)
	inst.components.workable:SetOnFinishCallback(onhammered)
	inst.components.workable:SetOnWorkCallback(onhit)

	inst:AddComponent("hauntable")
	inst.components.hauntable:SetHauntValue(TUNING.HAUNT_TINY)
	
	inst.vault = {}

	inst:ListenForEvent("onbuilt", onbuilt)
	MakeSnowCovered(inst)

	inst.OnSave = onsave 
	inst.OnLoad = onload
	
	updatelight(inst)

	return inst
end

 

Error log from last session

[string "../mods/Pandora - prealpha/scripts/prefabs/..."]:598: bad argument #2 to 'insert' (number expected, got table)
LUA ERROR stack traceback:
=[C]:-1 in (field) insert (C) <-1--1>
../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:598 in (field) OnSave (Lua) <590-601>
   inst = 107566 - strangebox (valid:true)
   data = table: 07CD99F0
   k = 1
   v = 113492 - cutreeds (valid:true)
scripts/entityscript.lua:1521 in (method) GetPersistData (Lua) <1489-1538>
   self (valid:true) =
      GUID = 107566
      Transform = Transform (1F72EFC8)
      inlimbo = false
      actionreplica = table: 1F8D53E0
      _faderate = net_smallbyte (1F8E4618)
      actioncomponents = table: 1F8D5228
      event_listening = table: 1F8D5FC0
      lower_components_shadow = table: 1F8D52A0
      loot = table: 07257A60
      lootaggro = table: 07257AD8
      entity = Entity (1F855120)
      AnimState = AnimState (1F72EE68)
      prefab = strangebox
      Light = Light (1F72EE88)
      children = table: 1F8F0358
      OnSave = function - ../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:590
      Network = Network (1F72EF68)
      persists = true
      OnLoad = function - ../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:603
      MiniMapEntity = MiniMapEntity (1F72EEE8)
      pendingtasks = table: 1F8D5818
      SoundEmitter = SoundEmitter (1F72EFE8)
      vault = table: 1F8D67E0
      _fadeval = net_float (1F8E4880)
      name = Ornate Chest
      last_prox_sfx_time = 0.43333335593343
      replica = table: 1F8D52C8
      spawntime = 0
      components = table: 1F8D5200
      event_listeners = table: 1F8D59D0
   references = table: 07CD9E00
   data = table: 07CD99F0
scripts/entityscript.lua:288 in (method) GetSaveRecord (Lua) <243-293>
   self (valid:true) =
      GUID = 107566
      Transform = Transform (1F72EFC8)
      inlimbo = false
      actionreplica = table: 1F8D53E0
      _faderate = net_smallbyte (1F8E4618)
      actioncomponents = table: 1F8D5228
      event_listening = table: 1F8D5FC0
      lower_components_shadow = table: 1F8D52A0
      loot = table: 07257A60
      lootaggro = table: 07257AD8
      entity = Entity (1F855120)
      AnimState = AnimState (1F72EE68)
      prefab = strangebox
      Light = Light (1F72EE88)
      children = table: 1F8F0358
      OnSave = function - ../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:590
      Network = Network (1F72EF68)
      persists = true
      OnLoad = function - ../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:603
      MiniMapEntity = MiniMapEntity (1F72EEE8)
      pendingtasks = table: 1F8D5818
      SoundEmitter = SoundEmitter (1F72EFE8)
      vault = table: 1F8D67E0
      _fadeval = net_float (1F8E4880)
      name = Ornate Chest
      last_prox_sfx_time = 0.43333335593343
      replica = table: 1F8D52C8
      spawntime = 0
      components = table: 1F8D5200
      event_listeners = table: 1F8D59D0
   record = table: 07CD93B0
   references = nil
scripts/mainfunctions.lua:678 in (global) SaveGame (Lua) <658-786>
   isshutdown = true
   cb = function - scripts/mainfunctions.lua:1060
   save = table: 06C7A708
   nument = 1319
   saved_ents = table: 06C7A398
   references = table: 06C7A550
   k = 107566
   v = 107566 - strangebox (valid:true)
   x = -113.06099700928
   y = 0
   z = 203.61099243164
scripts/saveindex.lua:343 in (method) SaveCurrent (Lua) <332-344>
   self =
      data = table: 09BE8FF0
      current_slot = 1
   onsavedcb = function - scripts/mainfunctions.lua:1060
   isshutdown = true
   slotdata = table: 09BE8ED8
scripts/mainfunctions.lua:1591 in () ? (Lua) <1582-1593>

[00:01:56]: [string "../mods/Pandora - prealpha/scripts/prefabs/..."]:598: bad argument #2 to 'insert' (number expected, got table)
LUA ERROR stack traceback:
    =[C]:-1 in (field) insert (C) <-1--1>
    ../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:598 in (field) OnSave (Lua) <590-601>
    scripts/entityscript.lua:1521 in (method) GetPersistData (Lua) <1489-1538>
    scripts/entityscript.lua:288 in (method) GetSaveRecord (Lua) <243-293>
    scripts/mainfunctions.lua:678 in (global) SaveGame (Lua) <658-786>
    scripts/saveindex.lua:343 in (method) SaveCurrent (Lua) <332-344>
    scripts/mainfunctions.lua:1591 in () ? (Lua) <1582-1593>
	

 

strangebox.lua

 

Pandora.zip

Edited by ShinyMoogle
Added attachment
Link to comment
Share on other sites

The reason it crashes is the value of the table you're trying to insert is not a proper argument of the method table.insert.

../mods/Pandora - prealpha/scripts/prefabs/strangebox.lua:598 in (field) OnSave (Lua) <590-601>
   inst = 107566 - strangebox (valid:true)
   data = table: 07CD99F0
   k = 1
   v = 113492 - cutreeds (valid:true)

 

We're looking at the last line, v = 113492 - cutreeds (valid:true)
The compiler(interpreter) considered "113492 - cutreeds" as a number((meta)table actually) value because it starts with numeric. 

Quote

[string "../mods/Pandora - prealpha/scripts/prefabs/..."]:598: bad argument #2 to 'insert' (number expected, got table)

#2 argument of 'insert' is [, pos] which must be number. So the error happens.


However, to solve the problem, I need to know how inst.vault is filled.
Full-script of your mod or Github reference would help.

Link to comment
Share on other sites

Sure - attached full script to original post. That explains the "number expected" error - I was wondering about that.

inst.vault is filled in the container's OnClose function. It goes through each item slot in the container and, if the slot has an item that isn't tagged as "irreplaceable" and passes an RNG check, removes the item from the chest and uses table.insert to shunt it into inst.vault instead.

Items in inst.vault then have a chance to be filled back into empty slots using table.remove in the OnOpen function.

Edited by ShinyMoogle
Link to comment
Share on other sites

Sure, thanks for looking this over. Attached full mod directory to first post.

EDIT: May actually have gotten something working. Testing now.

Alright, going to call this tentatively solved. I was able to work around the "number expected" error by writing to the save data table directly instead of using table.insert.

local function onsave(inst, data)
    if inst.components.burnable ~= nil and inst.components.burnable:IsBurning() or inst:HasTag("burnt") then
        data.burnt = true
    end
	
	data.vault = {}
	
    for k,v in pairs(inst.vault) do
        if v:IsValid() and v.persists then --only save the valid items
			data.vault[k] = v:GetSaveRecord()
        end
    end
end

local function onload(inst, data)
    if data ~= nil and data.burnt and inst.components.burnable ~= nil then
        inst.components.burnable.onburnt(inst)
    end
	
	if data and data.vault then
		for k,v in pairs(data.vault) do
            local item = SpawnSaveRecord(v)
            if item then
				if item:IsValid() then
					print ("Valid item.")
					if item.components.perishable then
						item.components.perishable:StopPerishing()
					end
					table.insert(inst.vault, item)
				else
					print ("Item is invalid! Not loaded into chest vault. ")
				end
				print (item)
            end
        end
	end
end

 

One unintended effect that did turn up though is that Ash will consistently pop an error saying that it's not valid. I had to code in a manual workaround to check if the item is still valid when re-filling chest slots. I suspect it's because ash will disappear if not in inventory or container, and when archived, an item is neither. This will have to do for now unless I figure out a workaround.

EDIT EDIT: Looking at print logs, seems ash is valid on game load, but will become not valid at some point, presumably due to time passing. Currently untested, but I'm checking for the disappears component and calling Disappears:StopDisappear() when filling the vault from save data. I think that should do it.

strangebox.lua

Edited by ShinyMoogle
Link to comment
Share on other sites

I think it is not efficient to solve this in problem-specific. Figuring out the exact cause is difficult to find because we wouldn't know how entities are being valid or not.

function EntityScript:IsValid()
    return self.entity:IsValid()
end

IsValid() is the direct callback from the engine side of the game which is not able to access.
 

Fortunately, I've found a clue what could cause this issue.

table.insert (inst.vault, inv:RemoveItemBySlot(i))

Line in onclose(), RemoveItemBySlot() returns item in container while removing itself from the container.

item.components.inventoryitem:OnRemoved()

However, this line calls; OnRemoved is used to being pulled out from the inventory(container). Which means,

function InventoryItem:OnRemoved()
    if self.owner then
        self.owner:RemoveChild(self.inst)
    end
    self:ClearOwner()
    self.inst:ReturnToScene()
    self:WakeLivingItem()
end

The item will "wake up", leaving its LIMBO state.
LIMBO is... I don't know.. I can't match the spelling because I'm not a native in English. But is the state when the object is being put inside the inventory. Normally, if the item is going to the other inventory, it will then be back to LIMBO state again. But the written code didn't.
I checked that item in the vault is out of LIMBO state and I assume the ash issue is derived from here.


Also, I'm not sure you're overlooked about it or just intended but it is permanently losing items' reference. In OnSave in Container.lua, 

data.items[k], refs = v:GetSaveRecord()
if refs then
  for k,v in pairs(refs) do
    table.insert(references, v)
  end
end

It clearly saves outer references so I think you should follow this code.
Although I don't see examples that leave ref except for the character entity.

Edited by YakumoYukari
Link to comment
Share on other sites

Good catch on the wake item/return to scene part. I didn't think to look there in InventoryItem. I do want the item to be removed from the accessible container, so I suspect it'll involve calling *.components.inventoryitem:RemoveFromScene() somewhere.

As for the references, that was intended in the sense that I had absolutely no idea what the game does with references and how they're called again... so I just didn't use them. Clearly not the best coding practice. :confused: More specifically, OnSave/OnLoad seems to be called differently between components and prefabs, and none of the prefab files actually return anything in their OnSave functions, so I figured I'd do likewise.

Still, that's probably not a good idea and might cause some unforeseen issues. I'll say it seemed to work fine from cursory testing.

Anyway really I was just trying to avoid needing to write up the extra code for a component, since I think that would be best if I were to follow the Container.lua code. But.. I guess I should. So I did. It's not the most efficient code right now (there are a few redundant calls to count table items), but it seems to be working as intended, and I added functionality to dump all hidden stash items when the chest is worked/destroyed so items aren't lost.

So for the time being, I think this will do.

archive.lua

strangebox.lua

Link to comment
Share on other sites

Each OnSave/OnLoad is called in the same function. 

Saves/Loads entity's components first and then the prefab's. You can see this in the method GetPersistData() and SetPersistData()

Also, the entity's OnPreload part is loaded before any other components' data to be loaded.

function EntityScript:SetPersistData(data, newents)
    if self.OnPreLoad ~= nil then
        self:OnPreLoad(data, newents)
    end

    if data ~= nil then
        for k, v in pairs(data) do
            local cmp = self.components[k]
            if cmp ~= nil and cmp.OnLoad ~= nil then
                cmp:OnLoad(v, newents)
            end
        end
    end

    if self.OnLoad ~= nil then
        self:OnLoad(data, newents)
    end
end

Well anyways. Hope no more errors to appear.

Edited by YakumoYukari
Link to comment
Share on other sites

Oh. Ohhh, I see. I was having some difficulty following the entityscript logic but I'm kinda starting to see how it comes together. I guess I didn't really need to make a component for the one thing. ... Well, that's done so I'm happy if it's working. Thanks again!

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...