oopluacomputercraft

Lua (CC) GUI class draws all components in the same window when told to draw them in separate windows


Prelude

ComputerCraft is a mod for Minecraft (Forge) that adds a crude lua-based Computer to the game. Using this Computer, one can write programs to interact with the Minecraft world in various ways. Whether a ComputerCraft question is applicable to StackOverflow has been previously debated in other questions, but I believe it is applicable, as the mod is, for the most part, about programming, and while some ComputerCraft proprietary API calls are made, there is no concept in this question that would not apply to other, non-ComputerCraft-related lua programs (unless of course the problem is caused by a bug in ComputerCraft itself). Documentation for the used APIs can be found at http://www.computercraft.info/wiki/Category:APIs.

Note: Do not be alarmed if you have no ComputerCraft experience; I believe that this issue may be completely unrelated to ComputerCraft, and instead be caused by some intricacy of OOP in lua that I have failed to grasp. I have commented the code where I felt it necessary to explain the most important aspects of the proprietary calls I am making. If anything is unclear, please comment and I will clarify.

If you want to be able to run the code examples without Minecraft, there is an excellent ComputerCraft emulator available called CCEmuRedux. I have tested my code on both actual ComputerCraft and CCEmuRedux with identical results, although CCEmuRedux doesn't seem to support Monitors. An "Advanced" Computer is necessary to see the colours.

Problem

In ComputerCraft 1.75 (and CCEmuRedux @ ComputerCraft 1.79), given the following class gui, and a test program that attempts to draw a rudimentary button in each of two different windows using the gui class, both buttons are drawn in the second window. Graphically, the result of guiTest.lua is https://i.sstatic.net/xKHAV.png, while I would expect the first (orange) button to be drawn in Window 1. While I have some theories as to why it behaves this way, I don't have the necessary lua experience to figure out how to fix it. This is an MWE.

Code example

gui.lua

--Meta class
gui = {t, vpx, vpy}

function gui:new(t, title) -- I'm aware this constructor is not in keeping with the referenced Tutorialspoint article, it is of no consequence in this example
    local o = o or {}
    setmetatable(o, self)
    self.__index = self
    self.t = t
    local sX, sY = self.t.getSize() -- get the size of the virtual terminal and save it to vpx, vpy
    self.vpx = sX
    self.vpy = sY
    self.t.setCursorPos(1, 1) -- put cursor at the start of the virtual terminal
    self.t.write(tostring(title)) -- note that this WORKS, it prints one title per Window as seen in the screenshot
    return o
end

function gui:drawButton(x, y, sX, sY, colour)
    self.t.setCursorPos(x, y) -- set the cursor to the button's first x- and y-coords
    self.t.setTextColor(colours.black) -- set text colour to black
    self.t.setBackgroundColor(colour) -- set background colour to the colour of the button
    for iY = 1, sY do 
        for iX = 1, sX do
            self.t.write("#") -- print hashtags to represent the button until we reach sX and sY
        end
        self.t.setCursorPos(x, y + iY) -- move cursor a line down, and back to button's first x-coord
    end
    self.t.setCursorPos(self.vpx, self.vpy) -- get cursor out of the way so the screenshot will be prettier
end

guiTest.lua

dofile('gui.lua')

local w1 = window.create(term.current(), 2, 2, 22, 15)
local w2 = window.create(term.current(), 26, 2, 22, 15) -- creates virtual windows in a terminal, acting as terminals of their own
-- window.create() arguments: terminal object to create window on, x position, y position, x size, y size

local g1 = gui:new(w1, "Window 1") -- create gui object for the first window
local g2 = gui:new(w2, "Window 2") -- create gui object for the second window

g1:drawButton(5, 3, 3, 2, colours.orange) -- should draw in w1, draws in w2
g2:drawButton(10, 8, 4, 4, colours.green) -- should draw in w2, draws in w2

Attempted solutions

For what it's worth, I've been following the Lua OOP recipe @ https://www.tutorialspoint.com/lua/lua_object_oriented.htm. This is my second lua-based program, so I expect it to be an "easy" problem. I have more than a basic understanding of how OOP works in several other languages (particularly Java), though, and as such my programmer's "Spidey-Sense" is telling me that either some variable, such as t, isn't "local enough" (same variable gets used by both windows), or some reference in one of the gui objects gets overwritten when a new gui object gets created.

Therefore, I tried making the table gui local, to ensure it was not being overwritten:

local gui = {t, vpx, vpy}

... but it spat an error attempt to index ? on line 6 of "gui.lua" (setmetatable(o, self)), so instead I tried (realising that I would be unable to access the function from outside gui.lua, due to it being local):

local function gui:drawButton(x, y, sX, sY, colour)

... which resulted in guiTest.lua:1: bios.lua:14 [string "gui.lua"]:17:'(' expected. Line 17 is the definition of gui:drawButton() in the code tag above. In my admittedly limited ComputerCraft experience, such poorly formatted error messages generally mean that the lua interpreter or CraftOS is Exceptionally Confused™, but I assume the gist of it is "you can't make an object method local", as I can make other functions local in a similar fashion to what I've tried here.

It is not a problem with window.create() or with using the window API in general, as the same thing happens when using separate Monitors instead of just separate windows on the same Monitor. Essentially:

dofile('gui.lua')

local w = window.create(term.current(), 2, 2, 22, 15)
local m = peripheral.wrap('top') -- m becomes the Monitor physically on top of the ComputerCraft Computer

local gw = gui:new(w, "Window") -- create gui object for the Window
-- m is a terminal object, just like w, so we can still do
local gm = gui:new(m, "Monitor") -- create gui object for the Monitor

gw:drawButton(5, 3, 3, 2, colours.orange) -- should draw in w, draws in m
gm:drawButton(10, 8, 4, 6, colours.green) -- should draw in m, draws in m

Perhaps there is a way of storing the function as a local variable, along the lines of

local gui:printFoo = function() print("foo") end 
self:printFoo() -- prints "foo"...?

... or perhaps more likely, the issue is something I have entirely missed.

Conclusion

To make a long question short, defining two gui objects, one for each of two virtual console windows, and attempting to draw one button on each of the virtual console windows using their respective gui objects, results in both buttons being drawn on the same virtual console window. Why?


Solution

  • Yes, OOP in Lua is hard for Lua beginners, despite of excellent knowledge of OOP languages (such as Java).

    --Meta class
    gui = {}  -- class is a global variable, no default properties exist
    
    function gui:new(t, title)   -- t = window, self = your class "gui"
        local o = {}   -- creating NEW object
        setmetatable(o, self)  -- link the object with the class
        self.__index = self
        o.t = t        -- save window into object (not into class)
        local sX, sY = t.getSize() -- get the size of the virtual terminal
        o.vpx = sX  -- save window's properties into object (not into class)
        o.vpy = sY
        t.setCursorPos(1, 1)
        t.write(tostring(title)) 
        return o
    end
    
    function gui:drawButton(x, y, sX, sY, colour)  -- self = object
        ....
    end