This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Searous's profile picture

Problems with metatable objects inside tables

Started by Searous, 22 August 2015 - 02:51 PM
Searous #1
Posted 22 August 2015 - 04:51 PM
Over the past week or so, I've been working on a somewhat simple menu-based operation system. All so I can get used to working with while loops, events, and file managing. For the last few days, I've been trying to switch from having to write the same code over and over every time I want to add a text field or something somewhere. So, in an attempt to start using objects more, I created a test object for a number picker. I call these things "elements", and if I can get this to work right, I will use them everywhere.

The number picker is supposed to, when selected, allow you to select a number within a a range specified when the object is instantiated. This worked when I was testing it, but when it came to the selection part I decided it would be easier to make elements be relative to the button used to activate them. This is because I already do selection this way.

Inside my screens table, relative to the button I want to have an element associated with it, I create an variable and set it to a new NumberPicker object:

element = NumberPicker.new(0,0,4,14)
The first number is the selector's starting value. The second and third are the range of number that can be selected. The fourth is the button ID associated with the object. The reason I don't what to use that to set elements relative to buttons is because it would be much easier to do it this way, instead of rewriting some of my selection and drawing code. When I call this variable later inside, and outside the table it's in, I get an "attempt to index ? (a nil value)" error on the line that element is defined on.

Here is the NumberPicker class. I did my best with indention, but Notpad++ hates me:

local NumberPicker = {}
NumberPicker.__index = NumberPicker
NumberPicker.default = 1
NumberPicker.min = 1
NumberPicker.max = 9
NumberPicker.selected = 1
function NumberPicker.new(default,min,max,buttonID)
  local this = setmetatable({}, NumberPicker)
this.default = default
this.selected = default
this.min = min
this.max = max
this.active = false
this.buttonID = buttonID
return this
end
function NumberPicker.action(this)
  this.active = true
  while this.active do
   local event,p1,p2,p3 = os.pullEvent()
  if event == "key" then
	if p1 == keys.left then
	 this.selected = this.selected - 1
   elseif p1 == keys.right then
	 this.selected = this.selected + 1
   end
  end

   local ok,data = pcall(drawScreen)
  if not ok then
	bError = true
	local ok,data = pcall(drawError, data)
   if not ok then
	 os.shutdown()
   end
  end
  local ok1,data1 = pcall(drawSpecial)
  if not ok then
	bError = true
   local ok,data = pcall(drawError, data)
   if not ok then
	 os.shutdown()
   end
  end
end
end
function NumberPicker.draw(this)
  color("field")
  --term.setCursorPos(25,(selected - lowest) + 3)
  write(this.selected)
end

Here is my screens table, tScreens:

local tScreens = {
  [1] = {"Main Menu",{
----Main Menu----
	[1] = {"Login","Login to an existing profile",0,
	  element = NumberPicker.new(1,1,20,1),
   action = function()
		tScreens[ 1 ][ 2 ][ 1 ].element:action()
	  end
	},
	[2] = {"Create Profile","Create a new profile for use on this system",0,
	  action = function()
		screen = 2
		lowest = 1
		highest = 5
		selected = 1
	  end
	},
	[3] = {"System Information","View information about this system",0,
	  action = function()
	
	  end
	},  
	[4] = {"Exit","Exit the system",0,
	  action = function()
	   term.setBackgroundColor(colors.black)
		bRunning = false
	  end
	}
  }},
  [2] = {"Create Profile",{
  ----Create Profile----
	[1] = {"Username","This name must be uniqe, and contain NO spaces",0,
	  action = function()
		color("field")
	  color("background")
	  term.setCursorPos(25,(selected - lowest) + 3)
	  
	  write("								   ")

	  term.setCursorPos(25,(selected - lowest) + 3)
	  tTempVars[1] = read()
	  end
	},
	[2] = {"Password","Chose a password",0,
	  action = function()
		color("field")
	  color("background")
	  term.setCursorPos(25,(selected - lowest) + 3)
	  
	  write("								   ")

	  term.setCursorPos(25,(selected - lowest) + 3)
	  tTempVars[2] = read("*")
	  end
	},
	[3] = {"Re-Password","Please retype the pasword you choes above",0,
	  action = function()
		color("field")
	  color("background")
	   term.setCursorPos(25,(selected - lowest) + 3)
	  
	  write("								   ")

	  term.setCursorPos(25,(selected - lowest) + 3)
	  tTempVars[3] = read("*")
	  end
	},
	[4] = {"Moto","Currently, this must remain short.  Keep it from going off the screen",0,
	  action = function()
		color("field")
	  color("background")
	  term.setCursorPos(25,(selected - lowest) + 3)
	  
	  write("								   ")

	  term.setCursorPos(25,(selected - lowest) + 3)
	  tTempVars[4] = read()
	  end
	},
	[5] = {"Text Color","The color of this text, and menu name text",0,
	  action = function()
		bLock = true
		nSelectedColor = tTempColors[1]
	  end
	},
	[6] = {"Field Color","The color of text fields",0,
	  action = function()
		bLock = true
		nSelectedColor = tTempColors[2]
	  end
	},
	[7] = {"Spacer Color","The color of the spacers",0,
	  action = function()
	  bLock = true
		nSelectedColor = tTempColors[3]
	  end
	},
	[8] = {"Cursor Color","The color of the cursor used to select options",0,
	  action = function()
		bLock = true
		nSelectedColor = tTempColors[4]
	  end
	},
	[9] = {"Tip Color","The color of info popups",0,
	  action = function()
	  bLock = true
		nSelectedColor = tTempColors[5]
	  end
	},
	[10] = {"Bad Color","The color of inaccesable fields, and locations",0,
	  action = function()
	  bLock = true
		nSelectedColor = tTempColors[6]
	  end
	},
	[11] = {"Menu Color","The color of menu buttons",0,
	  action = function()
	  bLock = true
		nSelectedColor = tTempColors[7]
	  end
	},
	[12] = {"Background Color","The color of the background",0,
	  action = function()
	  bLock = true
		nSelectedColor = tTempColors[8]
	  end
	},
	[13] = {"Cursor","The string of character(s) used as the cursor",0,
	  action = function()
	  color("field")
	  color("background")
	  term.setCursorPos(25,(selected - lowest) + 3)
	  
	  write("								   ")

	  term.setCursorPos(25,(selected - lowest) + 3)
	  tTempVars[5] = read()
	  tVars[5] = tTempVars[5]
	  end
	},
	[14] = {"Permission Level","The permission level of this user",4,
	  action = function()
		local ok, data = pcall(numberPicker)
	if not ok then
	  bError = true
	 status = data
	end
	  end
	},
	[15] = {"Create Profile","Accept the above settings, and create a new profile",0,
	  action = function()
	  if tTempVars[2] == tTempVars[3] and not fs.exists(".config_"..tTempVars[1]) then
		if bLoggedIn then
		  screen = 4
		 else
		  screen = 1
		end
		selected = 1
		lowest = 1
		highest = 5
  
		resetVars()
		resetColors()
	  else
		color("tip")
		term.setCursorPos(25,(selected - lowest) + 3)
		write("Invalid Login")
		sleep(3)
	  end
	  end
	},
	[16] = {"Cancel","Drop the above settings, and don't create a new profile",0,
	  action = function()
		screen = 1
		selected = 1
		lowest = 1
		highest = 5
	  resetColors("temp")
	  resetVars("temp")
	  end
	}
  }, {[14] = NumberPicker.new(0,0,4,14)}},
  [3] = {"Login",{
  
  }},
  [4] = {"Main Menu",{ --Deprecated
	[1] = {"test","test",0,
	 action = function()
  
	 end
   }
  }}
}

Here is my drawing code:

local function drawScreen()
  color("background")
  clear()

  color("text")
  print(tScreens[screen][1])

  color("spacer")
  print("---------------------------------------------------")
  
  color("menu")
  --Buttons
  local nTemp = lowest
  for i = 1, 5, 1 do
	color("menu")
	if tScreens[screen][2][nTemp] == nil then
	  print(" ")
	else
	  local string = ""
	  for i = 1, tVars[5]:len(), 1 do
		string = string.." "
	  end
	  if tVars[6] < tScreens[ screen ][ 2 ][ nTemp ][ 3 ] then
		color("bad")
	  end
	  print(string.." "..tScreens[ screen ][ 2 ][ nTemp ][ 1 ])
	end
	
  nTemp = nTemp + 1
  end
  ---------
  color("spacer")
  print("---------------------------------------------------")

  color("text")
  print(tScreens[ screen ][ 2 ][ selected ][ 2 ])

if selected >= lowest and selected <= highest then
	term.setCursorPos(1,(selected - lowest) + 3)
	color("cursor")
	write(tVars[5])
end
end
local function drawSpecial()
  term.setCursorPos(x - tVars[1]:len() + 1,1)
  write(tVars[1])
  term.setCursorPos(1,1)
  if lowest > 1 then
	term.setCursorPos(x,3)
	write("*")
	term.setCursorPos(1,1)
  end
  if highest < getTableSize(tScreens[ screen ][ 2 ]) then
	term.setCursorPos(x,7)
	write("*")
	term.setCursorPos(1,1)
  end
  if debug then
color("field")
	term.setCursorPos(x - 3,y)
	write("h"..highest)
	term.setCursorPos(x - 7,y)
	write("l"..lowest)
	term.setCursorPos(x - 11,y)
	write("s"..selected)
	term.setCursorPos(1,1)
  end
  ----Screens----
  if screen == 2 then
	color("field")
   local nTemp = lowest
	for i = 1, 5, 1 do
	  term.setCursorPos(25, (nTemp - lowest) + 3)
	  if tTempVars[nTemp] == nil then
	  
	  else
		color("field")
		--write(tTempVars[nTemp])
	  end
	
	 if nTemp == 1 then
	  term.setCursorPos(25,(nTemp - lowest) + 3)
	   write(tTempVars[1])
	 elseif nTemp == 2 then
	  term.setCursorPos(25,(nTemp - lowest) + 3)
	
	  local string = ""
	  for i = 1, tTempVars[2]:len(), 1 do
		string = string.."*"
	  end

	  write(string)
	   elseif nTemp == 3 then
	   term.setCursorPos(25,(nTemp - lowest) + 3)
	
	  local string = ""
	  for i = 1, tTempVars[3]:len(), 1 do
		string = string.."*"
	  end

	  write(string)
	 elseif nTemp == 4 then
	   term.setCursorPos(25,(nTemp - lowest) + 3)
	   write(tTempVars[nTemp])
	 elseif nTemp == 13 then
	  color("field")
	  color("background")
	   term.setCursorPos(25,(nTemp - lowest) + 3)
	   write(tTempVars[5])
	 -- elseif nTemp == 14 then
	   -- color("field")
	  -- color("background")
	   -- term.setCursorPos(25,(nTemp - lowest) + 3)
	   -- write(tTempVars[6])
	 end
  
	  if nTemp >= 5 and nTemp <= 11 then
		if term.isColor then
	  if bLock and selected == nTemp then
			paintutils.drawBox(25,(nTemp - lowest) + 3,26,(nTemp - lowest) + 3,getColor(nSelectedColor))
		  else
			paintutils.drawBox(25,(nTemp - lowest) + 3,26,(nTemp - lowest) + 3,getColor(tTempColors[ nTemp - 4 ]))
		  end
	 end
	  end
	
	  if selected >= 5 and selected < 14 and bLock then
		color("cursor")
		color("background")
		term.setCursorPos(23,(selected - lowest) + 3)
		write("<")
		term.setCursorPos(28,(selected - lowest) + 3)
		write(">")
	  end
	
	  nTemp = nTemp + 1
	end
  
	term.setCursorPos(1,1)
  end
----Elements----
if tScreens[ screen ][ 3 ] ~= nil then
  local size = #tScreens[ screen ][ 3 ]
  local nTemp = lowest
  for i = 1, 5 do
   if tScreens[ screen ][ 3 ][ nTemp ] ~= nil then
	term.setCursorPos(25,(nTemp - lowest) + 3)
	 tScreens[ screen ][ 3 ][ nTemp ]:draw()
   end
  nTemp = nTemp + 1
  end
end
end

And here is my main while loop:

while bRunning do
  if not bStarted then
	selected = 1
	screen = 1
	local ok,data = pcall(registerElements)
  if not ok then
	bError = true
	 local ok,data = pcall(drawError,data)
   if not ok then
	 os.shutdown()
   end
  end
	bStarted = true
  end
------Input------
  if not bError then
  local event,p1,p2,p3 = os.pullEvent()
  if event == "key" then
   if p1 == keys.up or p1 == keys.w then
   ----Up----
	if not bLock then  
	 if selected == 1 then
	  selected = getTableSize(tScreens[screen][2])
	  if getTableSize(tScreens[screen][2]) > 5 then
	   lowest = getTableSize(tScreens[screen][2]) - 5
	   highest = getTableSize(tScreens[screen][2])
	  end
	 else
	  selected = selected - 1
	 end
	
	 if selected - lowest == 5 then
	  lowest = lowest + 1
	 elseif selected < lowest then
	  lowest = lowest - 1
	 end
	
	 if highest - selected == 5 then
	  highest = highest - 1
	 elseif selected > highest then
	  highest = highest + 1
	 end
	
   --	local ok, data = pcall(drawScreen)
   ---  if not ok then
   --	os.shutdwon()
   --  end
	-- local ok, data = pcall(drawSpecial)
	-- if not ok then
	--   os.shutdwon()
	--  end
	end
   elseif p1 == keys.down or p1 == keys.s then
   ----Down----
	if not bLock then
	 if selected == getTableSize(tScreens[screen][2]) then
	  selected = 1
	  lowest = 1
	  highest = 5
	 else
	  selected = selected + 1
	 end
	
	 if selected - lowest == 5 then
	  lowest = lowest + 1
	 elseif selected < lowest then
	  lowest = lowest - 1
	 end
	
	 if highest - selected == 5 then
	  highest = highest - 1
	 elseif selected > highest then
	  highest = highest + 1
	 end
	end
   elseif p1 == keys.enter or p1 == keys.space then
   ----Select----
	if not bLock then
	 if tVars[6] >= tScreens[ screen ][ 2 ][ selected ][ 3 ] then
	  tScreens[ screen ][ 2 ][ selected ]:action()
	  drawScreen()
	  drawSpecial()
	 end
	else
	 if screen == 2 then
	  if selected == 5 then
	   tTempColors[1] = nSelectedColor
	  end
	  bLock = false
	 end
	end
   end
  elseif event == "mouse_click" then
   if not bLock then
	if p1 == 1 then
	 if (p3 > 2 and p3 < 8) and tScreens[ screen ][ 2 ][ p3 - 2 ] ~= nil then
	  if p2 < (tVars[5].." "..tScreens[ screen ][ 2 ][ lowest + (p3 - 3) ][ 1 ]):len() + 1 then
	   if selected == lowest + (p3 - 3) then
		tScreens[ screen ][ 2 ][ selected ]:action()
	   else
		selected = lowest + (p3 - 3)
	   end
	  end
	 end
	end
   else
  
   end
  
  elseif event == "mouse_scroll" then
   if p1 == -1 then
	if not bLock then
	 if lowest == 1 then
	
	 else
	  lowest = lowest - 1
	  highest = highest - 1
	 end
	else
	
	end
	drawScreen()
	drawSpecial()
   elseif p1 == 1 then
	if not bLock then
	 if highest == getTableSize(tScreens[ screen ][ 2 ])  then
	
	 else
	  lowest = lowest + 1
	  highest = highest + 1
	 end
	else
	
	end
	drawScreen()
	drawSpecial()
   end
  end
else
   local ok, var = pcall(drawError, status)
  if not ok then
	color("bad")
   print("An unexpected error has occurred while throwing the error '"..status.."'")
   print(" ")
   print("The system will terminate in 10 seconds")
   sleep(10)
	os.shutdown()
  end
end
  if not bRunning then
   term.setBackgroundColor(colors.black)
	clear()
else
   local ok, data = pcall(drawScreen)
  if not ok then
	bError = true
   status = data
  end
  local ok, data = pcall(drawSpecial)
  if not ok then
   bError = true
   status = data
  end
  end
end

Lastly, here is a the pastebin code for the program as it currently is:

Djw10qEW
There is some code I don't use, and some I haven's used yet.

The question is, why does calling element throw nil?

Also, another problem I've been having is when I run the file at startup the screen won't draw until I click, or press a key.

Thank you for reading, and I hope you decide to help
- Searous
Bomb Bloke #2
Posted 23 August 2015 - 07:41 AM
local tScreens = {
  [1] = {"Main Menu",{
----Main Menu----
        [1] = {"Login","Login to an existing profile",0,
          element = NumberPicker.new(1,1,20,1),
   action = function()
                tScreens[ 1 ][ 2 ][ 1 ].element:action()

Here's a problem - your attempt to reference tScreens at the bottom of this snippet is going to be set to access the global space, because you haven't finished declaring it in the local one at the time of the function definition.

Try it as:

local tScreens
tScreens = {
  etc
Searous #3
Posted 23 August 2015 - 01:40 PM
local tScreens = {
  [1] = {"Main Menu",{
----Main Menu----
		[1] = {"Login","Login to an existing profile",0,
		  element = NumberPicker.new(1,1,20,1),
   action = function()
				tScreens[ 1 ][ 2 ][ 1 ].element:action()

Here's a problem - your attempt to reference tScreens at the bottom of this snippet is going to be set to access the global space, because you haven't finished declaring it in the local one at the time of the function definition.

Try it as:

local tScreens
tScreens = {
  etc
This doesn't seem to work properly. Now all I get is nothing. The computer just shuts down when I select the button.

At first, I had tried to call element from inside the table, without going through the table. Also, I can't access element anywhere else. It's as though it's not even being created.

Also, I tried putting the elements relative to screen, not button, and that worked find. It look something like this, and it was stuck at the end of one of the button lists.

[14] = NumberPicker.new(0,0,4,14)
I also found a problem with my code that draws the elements. It's supposed to reference an element using a for loop to iterate over visible button IDs. Here is that fixed element drawing. However, I get a nil when this is present. It's likely that I need another nil check in there, but I don't know.

for i = 1, 5 do
  if tScreens[ screen ][ 2 ][ nTemp ].element ~= nil then 
   term.setCursorPos(25,(nTemp - lowest) + 3)
   tScreens[ screen ][ 2 ][ nTemp ].element:draw()
  end
nTemp = nTemp + 1
end
nTemp is set to the current value of lowest. This is how I can infer what button I need to draw and when I need to do so.

Chanced are I'm overlooking something somewhere. It just doesn't make sense that I am unable to reference element anywhere.
Bomb Bloke #4
Posted 24 August 2015 - 02:22 AM
This doesn't seem to work properly. Now all I get is nothing. The computer just shuts down when I select the button.

Because your NumberPicker action function is rigged to shut the system down in the case of errors, yes, meaning you don't get to see much at all. Granted, it's supposed to call drawError first, but… because you're making the same mistake as the one I previously pointed out, even that won't work: you're defining a function that wants to call drawError before you define drawError itself within the local space. NumberPicker.action() will therefore refer to it in the global space, where it'll always be nil.

All references to a local variable must come after the declaration of that local variable. Go through the script and ensure that all such references are performed in the correct order.
Searous #5
Posted 24 August 2015 - 12:49 PM
Because your NumberPicker action function is rigged to shut the system down in the case of errors, yes, meaning you don't get to see much at all. Granted, it's supposed to call drawError first, but… because you're making the same mistake as the one I previously pointed out, even that won't work: you're defining a function that wants to call drawError before you define drawError itself within the local space. NumberPicker.action() will therefore refer to it in the global space, where it'll always be nil.
Yes, I understand that. I created a function above the tScreens table that called drawScreen. I then referenced that in tScreens. It worked.
Also, I haven't gotten to the part where I need to worry about NumberPicker.action yet as element inside tScreens is still nil. I don't understand why it is nil if I give it an actual name, but not when I give it a numeric index.

EDIT:
Okay, so I looked over my code, and yes you were right about NumberPicker.action causing an error. I did my error catching wrong there, and I fixed it. However, element is still nil outside the tScreens table. I can't reference it anywhere else. Why is this?
Edited on 24 August 2015 - 04:17 PM