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

personnal API and various bugs

Started by LeB0ucEtMistere, 15 August 2013 - 12:56 PM
LeB0ucEtMistere #1
Posted 15 August 2013 - 02:56 PM
hello :)/>
I'm currently developping a little API called utils2d wich is grouping function to help me coding my home computer (which will be leading machines, computers, misc peripherals, …)
Here is the actual code of the API :
http://pastebin.com/K04gFR3X
Sorry if the style is not common but i'm from the C++ and i'm used to think in object oriented ! So i use a little bit too much the table to structure all my datas logically. that's mainly why i passed through a function to get my tables well structured :)/> but then it allows me to used it very easily, in two lines i can display a box of the choosen color and then use it for collision events with the mouse…
So then i made a program called "menuprincipal" which is sensed to be my main menu.
here is the code :
http://pastebin.com/4ddqQQtj
note : MCEvent stand for MouseClickEvent and not MineCraftEvent ;)/> and the last call to the program screen is made for clearing up my screen and reset the terminal output

So here is my problem : when i launch menuprincipal i get this error :
menuprincipal:4: attempt to index ? (a nil value)
I don't understand where it could came from. I've spend a couple of hours on the code and now my eye are too much tired to find the little things that makes all bug… :/ i'm sure it's obvious but i can't find it !
So if you have got any idea or suggestion i take it ;)/>

PS : i'm french so excuse my poor english ^^' and the french portions in the code :P/>

EDIT : ok after digging a little in the lua native oriented object syntax, i found that my code could have been rewritten in a much more better way for the function drawBox essentially, which could be a part of the box2d table, nevermind i will change this soon :)/> love the OO :P/>
Lyqyd #2
Posted 15 August 2013 - 10:45 PM
Split into new topic.
LeB0ucEtMistere #3
Posted 16 August 2013 - 12:01 PM
Hello :)/>
Sorry for the double post but the problem change as i rewrote a lot of my code and developped another little API to hold buttons in my programs.
Here is the API Button : (Object Oriented code)
Spoiler

-- [[ Buttons API create by LeB0ucEtMistere ]] --
-- [[ Button and ButtonManager description ]] --
--require the utils2d API made by LeB0ucEtMistere, contact at ... for more informations
os.loadAPI("/perso/utils2d")
--
-- Button class object which allow you to create your own buttons
--
--
function Button(x,y,w,h,callback,bckgcolor,outlinecolor)
	  
		if not bckgColor then
				outlineColor = colors.white
		end
		if not bckgColor then
				outlineColor = colors.white
		end
	  
		Button = {}
		Button.__index = Button
		Button['box'] = utils2d.Box2d(x,y,w,h,bckgcolor,outlinecolor)
		Button['callback'] = callback
		Button['drawn'] = false
		Button['draw'] = function(self)
		if not self.drawn then
		   self.box:draw()
		   self.drawn = true
		  end	
		end  
	  
return Button
end


--
-- Button Manager class object wich allows you to hold several buttons at the same time
--
--
--
function ButtonManager(table)
if not table then
  table = {}
end

ButtonManager = {}
ButtonManager.__index = ButtonManager
ButtonManager['buttons'] = table
ButtonManager['running'] = true
ButtonManager['draw'] = function(self)
		for i=1,#self.buttons do	
			if not self.buttons[i].drawn then
			 self.buttons[i].box:draw()
			 self.buttons[i].drawn = true
			end	
		end
		 end
ButtonManager['startListen'] = function(self)
		  while self.running do
		   event, param1, param2, param3 = os.pullEvent()
		
		   if event == "mouse_click" then
			
			  MCEvent = utils2d.getMCEvent(event, param1, param2, param3)
			  but = self.butIsClicked(event)
			  if but ~= nil then
	
			   -- launch in parallel this function itself and the callback of the clicked button
			   -- it is a recursive call to the function startListen() itself
			   -- it allows us to wait for another button to be pressed while the first action is in progress
	
			   parallel.waitForAll(self.startListen(), but.callback())
	
			  end
			
			 else if event == "button_stopListen" then
			
			   self.running = false -- probleme
			
			 end
			end
		   end
		  
   ButtonManager['stopListen'] = function(self)
		   os.queueEvent("button_stopListen", true)
		   end
		  
   ButtonManager['empty'] = function(self)
		  for k,v in pairs(self.buttons) do self.buttons[k]=nil end
		 end  
  
   ButtonManager['addButton'] = function(self, buttonTable)
		   for k,v in pairs(button) do self.buttons.insert(self.buttons, button[k]) end
		  end
		
	ButtonManager['removeButton'] = function(self, button)
		   --will be implemented soon
		  end
		
   ButtonManager['butIsClicked'] = function(self, MCEvent) -- private function which return a ref to the clicked button
			for i=1,#self.buttons do	
			  if self.buttons[i].drawn then
			   if utils2d.clickInBox(event,self.buttons[i].box)
			   return self.buttons[i]
			   end
			  end	
		  end
		  return nil
		   end
		
	  
			  
	  
return Box2d
end
with the pastebin if you want to give it a shot : http://pastebin.com/7SMRAMTM

And here is a main i wrote quickly to show how the API is supposed to work :
Spoiler

os.loadAPI("/perso/button")

term.clear()
screen = peripheral.wrap("right")
term.redirect(screen)

function b1f ()
--do something
end
function b2f ()
--do something
end
function b3f (bm)
--quit the programm by calling the stopListen function on the ButtonMannager
bm:stopListen()
end

bm = button.ButtonManager()

b1 = button.Button(1,2,10,5,b1f(bm),colors.red,colors.blue)
b2 = button.Button(9,2,10,5)
b3 = button.Button(20,2,10,5,colors.red)

buttonTable = {b1,b2,b3}

bm:addButton(buttonTable)



bm:draw()
bm:startListen()


term.setBackgroundColor(colors.black)
term.clear()
term.restore()

With this API i would liek to keep it simple but complete. that's why i create a ButtonManager wich is sense to hold my buttons, draw them all, and triggered their callback fonction when they are clicked on the touchscreen

My main problem comes in the function startListen() of the ButtonManager.
Because of the presence of an infinite loop in this function, i tought that providing another function to stop this loop would be necessary, so i wrote the function stopListen() which basically is made to be the callback of a button which would be designed to shutdown the computer for example. I also would a design that allows me to hold several buttons at the same time, so i decided to work with the parallel API.
Here comes the issue :
Let's follow my mind, imagine that i create a button with stopListen() as callback function, if i click it, the program get an event, and see it is a "mouse_click" type event so he launch the parallel with
-another instance of the function startListen to continue to listen to the others buttons
-and the callback of the button, aka the stopListen() function
so at this point the first instance of the function startListen() is in pause until the parallels both yields.
In the second instance of of the function startListen(), the program get an event of "button_stopListen" type so he change the bool running to false. Because of this, the loops ends and the second instance of the function startListen() ends too. By the way, the callback function as also ended. So the parallels have both yields so the first startListen() function resume. Here is the problem : in this function, the bool running isn't set to false because it is another instance so the program will stuck in this infinite loop and won't end …

To solve this i have think about 2 things :

-first, replace the bool running by a global variable initialized since the beginning of the program and accessible from everywhere. But i know that global variables are bad, and i don't want to do like that. I don't even know if it's technically possible … :/

-second idea, create a table filled of functions, one for each button object in my manager. These functions will just basically be made to call the callback of the button if he is clicked. Then i put all these functions in a parallel call, with a premade function to stop listening. it would be something like that :

function foofunction(self, ...)
	if "the button is clicked" then
	callbackFunction(...) --call the callback of the button
	end
end

local tableFunc = {}
for k,v in pairs(self.buttons) do
	tableFunc.insert(tableFunc, foofunction(self,...))
end
parallels.waitForAll(stopFunc(self),here goes the functions in tableFunc........)

The problem with this is that i don't know how to do it ^^' i don't know how to launch waitForAll with a variable number of params :/


So if peoples have idea, suggestion, great idea or just comments on my code, i take :D/> !!! Witch idea do your prefer, how could you make them work, … i need help ;)/>
LeB0ucEtMistere
Lyqyd #4
Posted 16 August 2013 - 12:31 PM
Using a parallel call seems like a rather odd design decision. Why do you need that? It makes far more sense to simply call the button's function without a parallel call.
LeB0ucEtMistere #5
Posted 16 August 2013 - 01:14 PM
Yes but this way doesn't allows me launching several button's functions at the same time . I need a way of multi-tasking to do this
After reading a bit more i finally try another solution with coroutines. here is the function :

ButtonManager['startListen'] = function(self)
		  local coroutines = {}
		
		  function listener(self, event)
		   while true do
			if (event[1]=="button_stopListen") then
			break
			end
		   end
		  end
		
		  function launchCallbackGeneric(self, event, callback)
		   if event[1] == "mouse_click" then
			
			  MCEvent = utils2d.getMCEvent(event, param1, param2, param3)
			  but = self.butIsClicked(event)
			  if but ~= nil then
			 but.callback()
			  end
			 end
		  end
		
		  local stopC = coroutine.create(listener)
		  for k,v in pairs(self.buttons) do table.insert(coroutines, coroutine.create(launchCallbackGeneric)) end
		  -- we make a coroutine for each buttons in the manager
		
		  local evt = {}
		  while self.running do
		
		   coroutine.resume(stopC, unpack(evt))
		   for k,v in pairs(coroutines) do coroutine.resume(coroutines[v], unpack(evt)) end
			
			 if coroutine.status(stopC) == "dead" then
			
			   break
			
			 end
			 evt = {os.pullEvent()}
			end
		   end
		  
		  

Is it a good idea ? i thinks it would work no ?

EDIT some explanations : the function listener() is a function that loop until he detect an event of "button_stopListen" type. the launchCallbackGeneric function is a generic function. i declare as much instance as there is buttons in the table. Because of the call to the function butIsClicked in the function, the function know witch callback to call. I declare several of theses fonctions in order to always keep one of them waiting for a button to be clicked. I don't know if it's very clear ^^' it seems quite confused. say if you need more explanations
Lyqyd #6
Posted 16 August 2013 - 02:54 PM
This is a bad idea. Your API should call the function associated with the button (if any) and nothing else. Your main program calling the API should deal with any multitasking (and I can nearly guarantee you that you don't need multitasking) that might actually be necessary. What are you trying to use this for? I think you are definitely going about it backwards, and making this far more complicated than it needs to be.
LeB0ucEtMistere #7
Posted 16 August 2013 - 03:41 PM
So you think i'd better let the user of the API deal with the multi tasking part ? … Actually i've made this program to hold 4 or 5 buttons that will activate/desactivate redstone systems like spawner, nuclear powerplant, redstone cells, quarry, … So i would like to be able to click one button, let it do his task and click another before he ends
Can you explain me how you see this different design you re talking me about or at least giving me some advices and tips to start well a new design
Thanks for your help
LeBoucEtMistere

EDIT : if i just add a selfmethod to the button that let's him be clicked and return when it's done, like that :

button=Button(...)
button:draw()
button:click()
then i could just make like that to hold several buttons no ? :

parallels.waitForAll(b1,b2,b3,...)
or something like that, do you thinks it's a good idea, is that what you were meaning by getting the multitasking out of the API ?
Lyqyd #8
Posted 16 August 2013 - 10:32 PM
You definitely don't need a coroutine for each button. Here's an example API: http://pastebin.com/pFHeia96. With this API, you could do something like this:


os.loadAPI("button")
b = button.new("top")
b:add("Pulse Redstone", nil, 2, 2, 28, 11, colors.red, colors.lime) --add each button with a few add calls.
b:draw()

--pre-declare a timer variable.
local rsTimer

while true do
  local event = {b:handleEvents(os.pullEvent())}
  if event[1] == "button_click" then
    --handle each button click, for example:
    if event[2] == "Pulse Redstone" then
	  b:toggleButton("Pulse Redstone")
	  if rsTimer then
	    rs.setOutput("back", false)
	    rsTimer = nil
	  else
	    rs.setOutput("back", true)
	    rsTimer = os.startTimer(0.5)
	  end
    end
  elseif event[1] == "timer" then
    if event[2] == rsTimer then
	  --toggle output and restart timer
	  rs.setOutput("back", not rs.getOutput("back"))
	  rsTimer = os.startTimer(0.5)
    end
  end
end

Notice that there's no multitasking involved, just simple event management, and yet it can respond to a button on the screen being clicked even while it's pulsing the redstone output. If you really need multitasking (which you probably don't, in this case), you'd be best off with a parallel call in the main program that starts one coroutine to handle the screen interactions, and one each for the various other functions. The screen interaction coroutine would change variables declared in the main program which each management coroutine would check and only continue their code as long as the variable was in the desired state. Once they finished (or were stopped), they'd wait for an event queued by the screen coroutine to start again.
LeB0ucEtMistere #9
Posted 17 August 2013 - 07:43 AM
thanks for your answer. i've take a look in your code and the API but it seems pretty hard for me to understand ^^' i will look deeper and take time to understand. Can i take inspiration of your API for mine ? i will try both solutions and see what is the most convenient for me, even if i take me time, i will learn more few things :)/> thanks for help :)/>