Page 1 of 1

[Solved] Multiplayer Events Sequence

Posted: Fri Oct 16, 2020 5:01 pm
by rookhaven
Sorry - this one's a doozy

Ask: Can anyone point me to the minimally required sequence of events I should account for to make multiplayer work for my mod? Between players joining/leaving (and then rejoining), new/save games I'm starting to get brain mush. I haven't even gotten to configuration changes yet.

Problem: When Player2 joins, leaves, and then joins my game a SECOND time, I get an error saying that a gui element already exists - and its referencing the very first child object of my custom player gui.

Error:

Code: Select all

Error while running event CityPeeps::on_player_joined_game (ID 43)
Gui element with name k2cp_main_table already present in the parent element.
Mod Structure:
  • My mod has a Civ class created at control stage - created once and commonly referenced for all players.
  • Among many other things, in Civ self.state, I have a guis table that manages player GUIs for multiplayer.
  • The guis table inserts in on_player_joined_game and removes in on_player_left with instantiations of a Gui class that is each player's gui.
Test Results:
  • After resolving all my self-made 'ID 10 T' bugs and assuming I've tested everything I should, I haven't been able to break singleplayer for my mod. This includes game save/loads, player respawn, creating, mining, and destroying custom entities with player/robots etc.
  • Multiplayer has never encountered any desyncs yet. To be honest, I'm not even sure what a desync look like. I'm testing with two of my own accounts.
  • Multiplayer GUI is updating as I want it to - common elements are displayed correctly and each player's clicks are unique to them. The common Civ class is being updated as it should and those changes are displayed in all player guis. Even the console messages and inventory updates for all players is working correctly.
  • As a player joins, my guis table seems to be adding a single instance correctly - with the player_index verified.
  • As a player leaves, my guis table seems to be removing the appropriate instance correctly - with the table count and player_index verified.
  • Then I had the same account that was player 2 join a second time and boom. The only way I could get a 'this gui element already exists' is if I'm mixing my signals between tracked instances (in which chase, how in the heck is everything else working fine) or there's some saved/cached version of player two still left in my game?
My brain hurtz me now. Here are the relevant snippets of code:

Civ definition:

Code: Select all

Civ = {}

function Civ:init()
	self.state = {
	...
	guis = {}, -- Manage each player's GUI
	}	
	global.civ_state = self.state
end

function Civ:load()
	if global.civ_state ~= nil then
		self.state = global.civ_state
		for _, academy in pairs(self.state.academies) do
			setmetatable(academy, {__index = Academy})
		end
		for _, city in pairs(self.state.cities) do
			setmetatable(city, {__index = City})
		end
		for _, hub in pairs(self.state.hubs) do
			setmetatable(hub, {__index = Hub})
		end
		for _, resort in pairs(self.state.resorts) do
			setmetatable(resort, {__index = Resort})
		end
		for _, gui in pairs(self.state.guis) do
			setmetatable(gui, {__index = Gui})
		end
		if global.actions then
			script.on_event(defines.events.on_tick, on_tick)
		end
	end
end

function Civ:player_joined(event)
	local player = game.players[event.player_index]
	if not (player and player.valid) then return end

	local new_gui = Gui:new()
	new_gui:init_gui(event)
	table.insert(self.state.guis, new_gui)
Civ:debug("Player Gui Size: "..#self.state.guis)
end

function Civ:player_left(event)
	for index, gui in ipairs(self.state.guis) do
		if (gui.state.player_index == event.player_index) then
			table.remove(self.state.guis, index)
		end
	end
	for index, gui in ipairs(self.state.guis) do
Civ:debug("Player Index Remaining Post On Left: "..gui.state.player_index)
	end
Civ:debug("Player Gui Size: "..#self.state.guis)
end
Gui definition:

Code: Select all

Gui = {}

function Gui:new(o)
	o = o or {}
	setmetatable(o, self)
	self.__index = self
	return o
end
	
function Gui:init_gui(event)
	local player=game.players[event.player_index]
	if not (player and player.valid) then return end

	self.state = {
		player_index = event.player_index,
		player = player,

		-- Cached frames/flows
		main_frame = nil,
		dtl_frame = nil,
	...

Re: Multiplayer Events Sequence

Posted: Fri Oct 16, 2020 5:26 pm
by eradicator
The easiest way is to just not care. Factorio doesn't usually need special mp handling. The only major difference between a player being "join" vs "not joined" is that their character entity doesn't exist when not joined. Everything else like the LuaPlayer or the gui state are completely unaffected by leave/join and thus there is no need to take special care about "leaving" events unless you're trying to optimize UPS for gui updates or something.

So you're error is probably exactly what it says in the message - you're trying to create an element on the "second" join that you already created on the "first" join.

Re: Multiplayer Events Sequence

Posted: Fri Oct 16, 2020 8:24 pm
by rookhaven
Ok thanks! One day, I will learn not to make assumptions about state.