[0.15.35] [Modding] Crash to Desktop

Place to get help with not working mods / modding interface.
Post Reply
User avatar
DedlySpyder
Filter Inserter
Filter Inserter
Posts: 253
Joined: Fri Jun 20, 2014 11:42 am
Contact:

[0.15.35] [Modding] Crash to Desktop

Post by DedlySpyder »

So, for a bit of background, I'm working on a generic library so I can create something like a tabbed UI, with buttons along the top and a pane that changes depending on the tab selected. This works fine on its own at the moment, but when I save the game and reload, trying to switch tabs causes an instant crash to desktop repeatedly. I added a print to the start of the event, and it doesn't print before the crash, so I'm not sure what exactly is causing it. I ran into issues with saving my functions (aka, it doesn't work), but that was still caught by the game.

The log doesn't show anything.

I've added the "mod" I was using to test this out.

To reproduce:
  • Click the "1" button in the top UI to spawn the tabbed list
  • Save the game
  • Reload the game
  • Click on any of the tabs
If the UI is destroyed and recreated (by pressing "2" then "1" again) it works perfected fine from there, so there is just something happening with the UI on load.


Edit: Adding source from control.lua here.

Code: Select all

GUI = {}
GUI.TabFunc = 
function(caption)
	return function(frame)
		frame.add{type="label", caption=caption}
	end
end
GUI.TabData = 
{
	name = "Tabbed_Frame",
	active = "Option_2",
	navButtons = 
	{
		{
			name="Option_1",
			caption="Option 1"
		},
		{
			name="Option_2",
			caption="Really Long Name"
		},
		{
			name="Option_3",
			caption="Op3"
		}
	},
	tabs = 
	{
		Option_1 = GUI.TabFunc("This is Option 1's tab"),
		Option_2 = GUI.TabFunc("This is Option 2's tab"),
		Option_3 = GUI.TabFunc("This is Option 3's tab")
	}
	--player = game.players[event.player_index]
	--parent = game.players[1].gui.top
}

script.on_event(defines.events.on_player_joined_game, function (event)
	local player = game.players[event.player_index]
	
	if not player.gui.top["Test"] then
		local test = player.gui.top.add{type="flow", name="Test", direction="horizontal"}
		test.add{type="button", name="Test1", caption="1"}
		test.add{type="button", name="Test2", caption="2"}
		GUI.Events.Add("Test1", GUI.Events.CreateTab)
		GUI.Events.Add("Test2", GUI.Events.DestroyTab)
	end
end)

GUI.Events = {}
GUI.Events.DestroyTab = function(event)
	GUI.TabData.player = game.players[event.player_index]
	Utils.TabLib.Destroy(GUI.TabData)
end

GUI.Events.CreateTab = function (event)
	GUI.TabData.player = game.players[event.player_index]
	Utils.TabLib.Create(GUI.TabData)
end

--Add a new GUI clicked event to the GUI event
-- @param name name of the element
-- @param func function to be called for it
GUI.Events.Add = function(name, func)
	global.GuiEvents = global.GuiEvents or {} --[name] = callback_function
	global.GuiEvents[name] = func
	--GUI.Events.Register()
end

--Remove a GUI clicked event from the GUI event
-- @param name name of the element
-- @param func function to be called for it
GUI.Events.Remove = function(name, func)
	--TODO
	
	--GUI.Events.Register()
end

script.on_event(defines.events.on_gui_click, function(event)
	game.players[1].print("Clicked"..event.element.name) --DEBUG
	local element = event.element
	
	for name, func in pairs(global.GuiEvents) do
		if name == element.name then
			global.GuiEvents[name](event)
			return nil
		end
	end
end)

----------------------------- UTILS ----------------------------------

Utils = {}
Utils.TabLib = {}
Utils.TabLib.Create = function(d)
	global.TabLibData = global.TabLibData or {} -- TODO on_init
	local data = Utils.TabLib.VerifyData(d)
	if data then
		if not Utils.TabLib.Verify(data) then
			Utils.TabLib.Draw(data)
			Utils.TabLib.RegisterEvents(data)
		end
	end
end

Utils.TabLib.Switch = function(d)
	local data = Utils.TabLib.VerifyData(d)
	if data then
		if Utils.TabLib.Verify(data) then
			Utils.TabLib.Destroy(data)
			Utils.TabLib.Draw(data)
		end
	end
end

Utils.TabLib.Destroy = function(d)
	local data = Utils.TabLib.VerifyData(d)
	if data then
		game.players[1].print("Destroying name: "..data.name) --DEBUG
		if Utils.TabLib.Verify(data) then
			data.parent[data.name].destroy()
			global.TabLibData[data.name][data.player.name] = nil
			game.players[1].print("Parent name: "..data.parent.name) --DEBUG
		end
	end
end

Utils.TabLib.Draw = function(data)
	local mainFrame = data.parent.add{type="frame", name=data.name, direction="vertical"}
	local navFlow = mainFrame.add{type="flow", name=data.name.."_nav", direction="horizontal"}
	
	for _, navButton in pairs(data.navButtons) do
		if data.active == navButton.name then
			navFlow.add{type="label", name=navButton.name, caption=navButton.caption, style="Proxy_Wars_tab_nav_active"}
		else
			navFlow.add{type="button", name=navButton.name, caption=navButton.caption, style="Proxy_Wars_tab_nav_inactive"}
		end
	end
	
	local tabFrame = mainFrame.add{type="frame", name=data.name.."_tab_frame", direction="vertical"}
	data.tabs[data.active](tabFrame)
	
	global.TabLibData[data.name] = global.TabLibData[data.name] or {}
	global.TabLibData[data.name][data.player.name] = data
end

Utils.TabLib.RegisterEvents = function(data)
	local navButtons = data.navButtons
	local func = function(event)
		local element = event.element
		--local elementName = element.name
		--local navFlow = element.parent
		
		--Only if a different tab was selected
		if string.find(element.style.name, "inactive") then
			data.active = element.name
			Utils.TabLib.Switch(data)
		end
	end
	
	for _, navButton in pairs(navButtons) do
		GUI.Events.Add(navButton.name, Utils.TabLib.OnGuiEvent)
	end
end

Utils.TabLib.OnGuiEvent = function(event)
	local element = event.element
	local player = game.players[event.player_index]
	
	--Only if a different tab was selected
	if string.find(element.style.name, "inactive") then
		local data = global.TabLibData[element.parent.parent.name][player.name]
		data.active = element.name
		Utils.TabLib.Switch(data)
	end
end

Utils.TabLib.Verify = function(data)
	if data.parent[data.name] then
		return true
	end
	return false
end

Utils.TabLib.VerifyData = function(d)
	local data = deepcopy(d)
	if data and data.name and data.navButtons and data.tabs then
		--Either player or parent is required
		--Make sure we have somewhere to draw the tab
		if data.parent then
			if not data.parent.valid then return nil end
			if not data.player then
				data.player = data.parent.player
			end
		else
			if not data.player then return nil end
			data.parent = data.player.gui.center
		end
		
		--Verify that each nav button has a tab
		for _, navButton in pairs(data.navButtons) do
			if navButton.name and navButton.caption then
				if not data.tabs[navButton.name] then return nil end
				if type(data.tabs[navButton.name]) ~= "function" then return nil end
			else
				return nil
			end
		end
		
		--Verify the active tab or set it
		if not data.active then
			data.active = navButtons[1].name
		end
		game.players[1].print("Valid data"..game.tick) --DEBUG
		return data
	end
	return nil
end

--From base game's util
function deepcopy(object)
    local lookup_table = {}
    local function _copy(object)
        if type(object) ~= "table" then
            return object
        -- don't copy factorio rich objects
        elseif object.__self then
          return object
        elseif lookup_table[object] then
            return lookup_table[object]
        end
        local new_table = {}
        lookup_table[object] = new_table
        for index, value in pairs(object) do
            new_table[_copy(index)] = _copy(value)
        end
        return setmetatable(new_table, getmetatable(object))
    end
    return _copy(object)
end
Attachments
Test_0.0.1.zip
(5.06 KiB) Downloaded 70 times
Last edited by DedlySpyder on Fri Oct 06, 2017 10:43 am, edited 1 time in total.

Rseding91
Factorio Staff
Factorio Staff
Posts: 13204
Joined: Wed Jun 11, 2014 5:23 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by Rseding91 »

Thanks for the report. You've built your mod in a way that isn't supported: you have to store any game references you get in the global table or during the save process it's going to point at invalid locations and become corrupt on loading.

I'm going to move this to the Mod Help section.
If you want to get ahold of me I'm almost always on Discord.

User avatar
DedlySpyder
Filter Inserter
Filter Inserter
Posts: 253
Joined: Fri Jun 20, 2014 11:42 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by DedlySpyder »

Rseding91 wrote:Thanks for the report. You've built your mod in a way that isn't supported: you have to store any game references you get in the global table or during the save process it's going to point at invalid locations and become corrupt on loading.

I'm going to move this to the Mod Help section.
Thanks for the quick reply. Where am I not storing references in global? I do have some static variables (the TabFunc and TabData) that I don't store in global, but they are just the basis for something else and get modified before I use them.

Rseding91
Factorio Staff
Factorio Staff
Posts: 13204
Joined: Wed Jun 11, 2014 5:23 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by Rseding91 »

DedlySpyder wrote:Thanks for the quick reply. Where am I not storing references in global? I do have some static variables (the TabFunc and TabData) that I don't store in global, but they are just the basis for something else and get modified before I use them.

Code: Select all

GUI.TabData.player = game.players[event.player_index]
If you want to get ahold of me I'm almost always on Discord.

Rseding91
Factorio Staff
Factorio Staff
Posts: 13204
Joined: Wed Jun 11, 2014 5:23 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by Rseding91 »

Regarding the crash: I'll fix it for 0.16 but the reason it's crashing is you've created a circular reference in the table you're passing to LuaGuiElement::add
If you want to get ahold of me I'm almost always on Discord.

User avatar
DedlySpyder
Filter Inserter
Filter Inserter
Posts: 253
Joined: Fri Jun 20, 2014 11:42 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by DedlySpyder »

Rseding91 wrote:

Code: Select all

GUI.TabData.player = game.players[event.player_index]
So, that code is only found in the handler for the "1" and "2" buttons, which after a reload still work fine.

I think the crash is happening somewhere around this block (or the function call from GuiEvents?):

Code: Select all

script.on_event(defines.events.on_gui_click, function(event)
	game.players[1].print("Clicked"..event.element.name) --DEBUG
	local element = event.element
	
	for name, func in pairs(global.GuiEvents) do
		if name == element.name then
			global.GuiEvents[name](event)
			return nil
		end
	end
end)
But, that is pulling directly from a table, which is indexed by element name and returns a function. Which that part is fine in reference to the "1" and "2" buttons after reload, but not the tabs.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by eradicator »

Admittedly i find your code a bit hard to read so i might be wrong about this but it looks like you're trying to store actual functions in the global table. This is not supported.
API Documentation wrote: Only specific data can be saved and loaded using this table:
  • Basic data: nil, strings, numbers, booleans
  • Tables, but not meta tables; tables with metatables become plain tables when saved and loaded.
  • References to builtin Factorio LuaObjects
Called here:

Code: Select all

global.GuiEvents[name](event)
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
DedlySpyder
Filter Inserter
Filter Inserter
Posts: 253
Joined: Fri Jun 20, 2014 11:42 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by DedlySpyder »

Ok, I've found the issue. You were close eradicator, it was an issue with saving functions but not those functions.

The problem ended up being this:

Code: Select all

GUI.TabFunc = 
function(caption)
	return function(frame)
		frame.add{type="label", caption=caption}
	end
end
My problem was that I was trying to do something quick and dirty to make multiple tabs. Since this function returns an anonymous function to the GUI.TabData.tabs table, on a reload it's not properly initialized. These tab functions are called when the active tab is redrawn by clicking a different tab.

For some reason when I add game.write_file{...} to that function it fails as expected from Factorio with the following error (which basically means that the function doesn't exist anymore):

Image

So, I'm not sure why it is a crash to desktop with just the frame.add{...}, but uncurrying that function seems to make it work just fine:

Code: Select all

GUI.TabFunc = 
function(frame)
	frame.add{type="label", caption="This is a tab"}
end
Thanks for the help guys.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 5206
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: [0.15.35] [Modding] Crash to Desktop

Post by eradicator »

DedlySpyder wrote:

Code: Select all

GUI.TabFunc = 
function(frame)
	frame.add{type="label", caption="This is a tab"}
end
Btw the most common coding style in factorio modding would look like this:

Code: Select all

function GUI.TabFunc (frame)
	frame.add{type="label", caption="This is a tab"}
end
Author of: Belt Planner, Hand Crank Generator, Screenshot Maker, /sudo and more.
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

Post Reply

Return to “Modding help”