Changing a table property of an OOP object via a method causes changes to replicate to all other objects

When a player joins the game, the server creates a player object and stores the created table to another ModuleScript, where it can be accessed from other scripts. Because this object stores a lot of information and I don’t want to make the post extremely long, let’s say the player object stores a reference to the actual Player, a table property containing a few values.

function playerEntity.new(params)
	local self = {}

	self.Player = params.Player
	self.PlayerValues = {
		Health = 200
	}
	
	return setmetatable(self, playerEntity) 
end

-- This is where the problem arises, I think
-- valuesTable contains a lot more key-value pairs in the actual game; it's why I'm using a for loop instead of just setting them equal one-by-one
function playerEntity:updatePlayerValues(valuesTable)
	for key, value in pairs(valuesTable) do
		self.PlayerValues[key] = value
	end
	
	return true
end

function playerEntity:setup()
	print("Hi")
end

The ModuleScript that stores the player objects is called PlayerList and looks like this:

local playerList = {}
playerList.Players = {}

function playerList:createNewEntry(playerEntityObject)
	if self.Players[playerEntityObject.Player.UserId] == nil then
		self.Players[playerEntityObject.Player.UserId] = playerEntityObject
	end
end

So to modify the properties of a player object, I would do PlayerList.Players [player.UserId].PlayerValues.Health = 300 or, using the method (which is what I will be using), PlayerList.Players[player.UserId]:updatePlayerValues({ Health = 300 }), for example.
This is what the server script for player joining would look like:

Players.PlayerAdded:Connect(function(player)
	local params = {
		Player = player
	}
	
	local playerEntity = PlayerEntity.new(params)
	playerEntity:setup() 
	PlayerList:createNewEntry(playerEntity) 
    -- Add to the list so we can access it for later
end)

Now, if I were to call updatePlayerValues from this server script (in my case, it’s when a RemoteFunction is invoked), to update a single player’s Health value, it instead updates all the players’ Health values.

-- This is in the same server script as the PlayerAdded connection
RemoteFunction.OnServerInvoke = function(player, healthValue) -- Say this is 400
	PlayerList.Players[player.UserId]:updatePlayerValues({ Health = healthValue })
        -- All the players will have their Health changed to 400

	for userId, info in pairs(PlayerList.Players) do
		print(info.PlayerValues.Health, userId)
        -- This would print 400 for all the players
	end
	return true
end

I honestly have no idea how any of this is even happening, and I’ve checked for hours to no avail. Help would be greatly appreciated. Please ask if any part isn’t clear enough.

1 Like

Why are you setting the metatable of the new playerEntity object to a table that doesn’t contain any metamethods? Should it not be this:

return setmetatable(self, {__index = playerEntity})

You don’t know how they define playerEntity, maybe it’s something like this:

local playerEntity = {}
playerEntity.__index = playerEntity

Nevermind. Though it must be somewhere, otherwise they would get errors and it wouldn’t even run…

Could you show what PlayerList.Players looks like by printing it?
Also could you then turn on “Show memory address for expandable tables” in the output?
image
So we can see if you’re maybe using the same table twice, thus updating them all.

Yes, playerEntity is defined as follows:

local playerEntity = {}
playerEntity.__index = playerEntity

PlayerList.Players looks like this in the output when testing locally with 2 players (I won’t be using the example class, and I’ve printed it when playerList:createNewEntry is called):
image

And as for the memory addresses of the PlayerValues tables… Yeah, they happen to be the same table. This is the output for a function I’ve added into PlayerList (once again, testing locally with 2 players):
image

The function is:

function playerList:debug()
	for userId, info in pairs(self.Players) do
		print(info.PlayerValues)
	end
end

And it’s called once before updating the PlayerValues table and another time after updating it when the RemoteFunction is invoked. If needed, I’ll just dump the entire playerEntity module.

1 Like

Can you try changing the return setmetatable(self, playerEntity) to this, it can help you debug if the PlayerValues table got changed to the same table.

local mt = {
    __index = playerEntity,
	__newindex = function(t, k, v)
		rawset(t, k, v)
		if k == "PlayerValues" then
			print(debug.traceback("PlayerValues has been modified"))
		end
	end
}

return setmetatable(self, mt)

I’ve just tried this, but __newindex doesn’t seem to be firing even though the PlayerValues table changed.

local playerEntity = {}
local mt = {
	__index = playerEntity,
	__newindex = function(t, k, v)
		rawset(t, k, v)
		if k == "PlayerValues" then
			print(debug.traceback("PlayerValues has been modified"))
		end
	end
}

function playerEntity.new(params)
	local self = {}
	
	self.PlayerValues = {}
	self.Class = params.Class or "Survivalist"
	
	self.Player = params.Player
	self.PlayerValuesFolder = nil
	
	self.PlayerCharacter = self.Player.Character
	self.UpdateCharacterConnection = nil
	
	self.Loadout = Loadout.new({Player = self.Player})

	return setmetatable(self, mt) 
end

As far as I know __newindex only runs when you set a value that is currently nil. So that won’t work here. Except if you maybe give .PlayerValues a metatable with that metamethod. However that of course won’t do anything if the entire table is overwritten.

If that doesn’t work then you should post every place in your code where .PlayerValues is accessed. Or atleast every place in the code where you do .PlayerValues = so we can see whether you are accidently overwriting it.

Alright. I should have done that in the original post.

The only time PlayerValues gets modified is when a player changes their class. This is done by firing a RemoteFunction, and a server script listens for its invocation.

ChangeClass.OnServerInvoke = function(player, requestedClass)
	
	if not ClassTable[requestedClass] then
		warn("Cannot change class: Requested class does not exist") 
		return false 
	else
		PlayerList:debug() -- prints the table
		PlayerList.Players[player.UserId]:updateClass(requestedClass)
		PlayerList:debug()
	end
	
	player.PlayerValues.Health.Value = 0 
	return true
end

ClassTable is a ModuleScript that contains dictionaries of PlayerValues. It looks like this:

local classTable = {}
classTable = {
	["Survivalist"] = { -- Class name
		["MaxHealthStatic"] = 150,
		["KnockbackResistance"] = 1	
        -- Contains a lot more but I've shortened it	
	},
}

The updateClass method calls updatePlayerValues, which is responsible for changing the contents of the PlayerValues table, and looks like this:

function playerEntity:updateClass(className)
	if ClassTable[className] == nil then
		warn("Unknown class: ", className)
	end
	
	self.Class = className
	self.Player.Class.Value = className
	self:updatePlayerValues(ClassTable[className])
end

updatePlayerValues:

function playerEntity:updatePlayerValues(valuesTable)
	if not self.PlayerValuesFolder then
		self:setupPlayerValues()
	end
	
	for key, value in pairs(valuesTable) do
		self:setPlayerValue(key, value)
	end
end

setPlayerValue, which calls playerValueExists:

function playerEntity:playerValueExists(key)
	if self.PlayerValues[key] == nil then
		warn(key .. " isn't a valid Player Value")
		return false
	end
	
	return true
end

function playerEntity:setPlayerValue(key, value)
	if self:playerValueExists(key) then
		self.PlayerValues[key] = value
	end
end

Finally, here’s the setup method that is called after the constructor is called (refer to the original post, 3rd code block).

function playerEntity:setup()
    -- The constructor sets self.PlayerValues = {}
	self.PlayerValues = DefaultPlayerValues.Values
    -- DefaultPlayerValues is another ModuleScript 
	
	self:setupClassValue()
	self:setupPlayerValues() -- This creates NumberValue objects and copies the values from self.PlayerValues to them (definitely isn't an issue)
	self:updateClass("Survivalist")
	
	self.UpdateCharacterConnection = self.Player.CharacterAdded:Connect(function(character)
		self:applyPlayerValues() -- Copies the values to the Value objects
		
		self.PlayerCharacter = character
		
        -- These don't matter
		self:setupHitboxes()
		self:setupBoundingBox()
		self:setupCollider()
		
		self.Loadout:apply()
	end)
end

I believe the issue lies in setup because printing PlayerList.Players and checking the PlayerValues tables when a player joins reveals that they’re actually all the same table.
image

Can you try changing it to this?

local function deepCopy(original)
  local copy = {}
  for k, v in pairs(original) do
    if type(v) == "table" then
      v = deepCopy(v)
    end
    copy[k] = v
  end
  return copy
end

self.PlayerValues = deepCopy(DefaultPlayerValues.Values)

The code will copy the default player values so its not the same. Source

1 Like

Wow, that works! I thought setting a table key equal to a ModuleScript table would set it equal to a copy of that table instead of a reference to the table, but I guess not. Thank you, everyone, for all your help and patience.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.