Created
November 27, 2021 03:35
-
-
Save cheapie/2039ae3274e7d629adf377b7777520d1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--Light Controller 2 | |
--A product of Advanced Mesecons Devices, a Cheapie Systems company | |
--This is free and unencumbered software released into the public domain. | |
--See http://unlicense.org/ for more information | |
local MODE_OFF = 0 | |
local MODE_ON = 1 | |
local MODE_DUSKDAWN = 2 | |
local MODE_DAWNDUSK = 3 | |
local MODE_GAMETIME = 4 | |
local MODE_REALTIME = 5 | |
local MODE_INTERNET = 6 | |
local INITSTATE_POWERON = 0 | |
local INITSTATE_HWDETECT_BLINKY = 1 | |
local INITSTATE_HWDETECT_LS = 2 | |
local INITSTATE_HWDETECT_RTC = 3 | |
local INITSTATE_HWDETECT_NIC = 4 | |
local INITSTATE_SUMMARY = 5 | |
local INITSTATE_READY = 6 | |
local SCREENSTATE_INIT = 0 | |
local SCREENSTATE_MAIN = 1 | |
local SCREENSTATE_STATUS = 2 | |
local SCREENSTATE_MODE = 3 | |
local SCREENSTATE_SETTINGS = 4 | |
local SCREENSTATE_ABOUT = 5 | |
local modestrings = { | |
[MODE_OFF] = "Always Off", | |
[MODE_ON] = "Always On", | |
[MODE_DUSKDAWN] = "Dusk to Dawn", | |
[MODE_DAWNDUSK] = "Dawn to Dusk", | |
[MODE_GAMETIME] = "In-Game Time", | |
[MODE_REALTIME] = "Real-Life Time", | |
[MODE_INTERNET] = "HTTP Remote", | |
} | |
local function describemode() | |
local ret = modestrings[mem.mode] | |
if mem.mode == MODE_DUSKDAWN then | |
return ret.."\\, on when light level is below "..mem.lighttrig | |
elseif mem.mode == MODE_DAWNDUSK then | |
return ret.."\\, on when light level is above "..mem.lighttrig | |
elseif mem.mode == MODE_GAMETIME or mem.mode == MODE_REALTIME then | |
return string.format("%s\\, on from %02d\\:%02d to %02d\\:%02d",ret,mem.ontime.hour,mem.ontime.min,mem.offtime.hour,mem.offtime.min) | |
else | |
return ret | |
end | |
end | |
local fs = "size[12,8]formspec_version[4]" | |
local function fsadd(element) | |
fs = fs..element | |
end | |
local function formatgametime(rtcoutput) | |
local ret = {} | |
rtcoutput = rtcoutput*24 | |
ret.hour = math.floor(rtcoutput) | |
ret.min = math.floor((rtcoutput-math.floor(rtcoutput))*60) | |
return ret | |
end | |
local function istimeinrange(current,start,stop) | |
local currhour = current.hour+(current.min/60) | |
local starthour = start.hour+(start.min/60) | |
local stophour = stop.hour+(stop.min/60) | |
if stophour > starthour then | |
return (currhour >= starthour and currhour < stophour) | |
elseif stophour < starthour then | |
return (currhour >= starthour or currhour < stophour) | |
else | |
return false | |
end | |
end | |
local function getdesiredstate() | |
if mem.mode == MODE_OFF then | |
return false | |
elseif mem.mode == MODE_ON then | |
return true | |
elseif mem.mode == MODE_DUSKDAWN then | |
return mem.lightlevel < mem.lighttrig | |
elseif mem.mode == MODE_DAWNDUSK then | |
return mem.lightlevel > mem.lighttrig | |
elseif mem.mode == MODE_GAMETIME then | |
return istimeinrange(formatgametime(mem.gametime),mem.ontime,mem.offtime) | |
elseif mem.mode == MODE_REALTIME then | |
return istimeinrange(os.datetable(),mem.ontime,mem.offtime) | |
elseif mem.mode == MODE_INTERNET then | |
return mem.url and mem.niccommand | |
end | |
end | |
if event.type == "program" or (mem.screenstate == SCREENSTATE_SETTINGS and event.channel == "touchscreen" and event.msg.defaults) then | |
mem.mode = MODE_OFF | |
mem.initstate = INITSTATE_POWERON | |
mem.screenstate = SCREENSTATE_INIT | |
mem.blinkypin = nil | |
mem.lsfound = false | |
mem.rtcfound = false | |
mem.nicfound = false | |
mem.url = nil | |
mem.lightstate = false | |
mem.ontime = {hour = 0,min = 0} | |
mem.offtime = {hour = 0,min = 0} | |
mem.lighttrig = 7 | |
mem.brightness = 14 | |
mem.offbrightness = 0 | |
elseif event.channel == "touchscreen" then | |
local fields = event.msg | |
if mem.screenstate == SCREENSTATE_INIT then | |
if mem.initstate == INITSTATE_POWERON and fields.next then | |
mem.initstate = INITSTATE_HWDETECT_BLINKY | |
interrupt(5,"blinkydetect") | |
elseif mem.initstate == INITSTATE_SUMMARY then | |
if fields.back then | |
mem.blinkypin = nil | |
mem.lsfound = false | |
mem.rtcfound = false | |
mem.nicfound = false | |
mem.initstate = INITSTATE_POWERON | |
elseif fields.next then | |
mem.initstate = INITSTATE_READY | |
mem.screenstate = SCREENSTATE_MAIN | |
end | |
end | |
elseif mem.screenstate == SCREENSTATE_MAIN then | |
if fields.viewstatus then | |
mem.screenstate = SCREENSTATE_STATUS | |
elseif fields.about then | |
mem.screenstate = SCREENSTATE_ABOUT | |
elseif fields.editsettings then | |
mem.screenstate = SCREENSTATE_SETTINGS | |
elseif fields.selectmode then | |
mem.screenstate = SCREENSTATE_MODE | |
end | |
elseif mem.screenstate == SCREENSTATE_SETTINGS then | |
if fields.cancel then | |
mem.screenstate = SCREENSTATE_MAIN | |
elseif fields.save then | |
mem.screenstate = SCREENSTATE_MAIN | |
fields.onhour = tonumber(fields.onhour) and math.floor(tonumber(fields.onhour)) | |
fields.onmin = tonumber(fields.onmin) and math.floor(tonumber(fields.onmin)) | |
fields.offhour = tonumber(fields.offhour) and math.floor(tonumber(fields.offhour)) | |
fields.offmin = tonumber(fields.offmin) and math.floor(tonumber(fields.offmin)) | |
fields.brightness = tonumber(fields.brightness) and math.floor(tonumber(fields.brightness)) | |
fields.offbrightness = tonumber(fields.offbrightness) and math.floor(tonumber(fields.offbrightness)) | |
fields.triggerlevel = tonumber(fields.triggerlevel) and math.floor(tonumber(fields.triggerlevel)) | |
if fields.onhour and fields.onhour >= 0 and fields.onhour <= 23 then | |
mem.ontime.hour = fields.onhour | |
end | |
if fields.onmin and fields.onmin >= 0 and fields.onmin <= 59 then | |
mem.ontime.min = fields.onmin | |
end | |
if fields.offhour and fields.offhour >= 0 and fields.offhour <= 23 then | |
mem.offtime.hour = fields.offhour | |
end | |
if fields.offmin and fields.offmin >= 0 and fields.offmin <= 59 then | |
mem.offtime.min = fields.offmin | |
end | |
if fields.triggerlevel and fields.triggerlevel >= 0 and fields.triggerlevel <= 14 then | |
mem.lighttrig = fields.triggerlevel | |
end | |
if fields.brightness and fields.brightness >= 0 and fields.brightness <= 14 then | |
mem.brightness = fields.brightness | |
end | |
if fields.offbrightness and fields.offbrightness >= 0 and fields.offbrightness <= 14 then | |
mem.offbrightness = fields.offbrightness | |
end | |
if fields.url then | |
mem.url = (string.len(fields.url) > 0) and fields.url | |
end | |
end | |
elseif mem.screenstate == SCREENSTATE_STATUS and fields.back then | |
mem.screenstate = SCREENSTATE_MAIN | |
elseif mem.screenstate == SCREENSTATE_ABOUT and fields.back then | |
mem.screenstate = SCREENSTATE_MAIN | |
elseif mem.screenstate == SCREENSTATE_MODE then | |
if fields.back then | |
mem.screenstate = SCREENSTATE_MAIN | |
elseif fields.mode then | |
if string.sub(fields.mode,1,3) == "CHG" then | |
local newmode = tonumber(string.sub(fields.mode,5,-1)) | |
local modes = {MODE_OFF,MODE_ON} | |
if mem.blinkypin then | |
if mem.lsfound then | |
table.insert(modes,MODE_DUSKDAWN) | |
table.insert(modes,MODE_DAWNDUSK) | |
end | |
if mem.rtcfound then | |
table.insert(modes,MODE_GAMETIME) | |
end | |
table.insert(modes,MODE_REALTIME) | |
if mem.nicfound then | |
table.insert(modes,MODE_INTERNET) | |
end | |
end | |
mem.mode = modes[newmode] | |
end | |
end | |
end | |
elseif (event.type == "on" or event.type == "off") and mem.initstate == INITSTATE_HWDETECT_BLINKY then | |
mem.blinkypin = event.pin.name | |
elseif event.iid == "blinkydetect" and mem.initstate == INITSTATE_HWDETECT_BLINKY then | |
mem.initstate = INITSTATE_HWDETECT_LS | |
digiline_send("sensor","GET") | |
interrupt(1,"lsdetect") | |
elseif mem.initstate == INITSTATE_HWDETECT_LS and event.channel == "sensor" then | |
mem.lsfound = true | |
mem.lightlevel = 0 | |
elseif event.iid == "lsdetect" and mem.initstate == INITSTATE_HWDETECT_LS then | |
mem.initstate = INITSTATE_HWDETECT_RTC | |
digiline_send("rtc","GET") | |
interrupt(1,"rtcdetect") | |
elseif mem.initstate == INITSTATE_HWDETECT_RTC and event.channel == "rtc" then | |
mem.rtcfound = true | |
mem.gametime = 0 | |
elseif event.iid == "rtcdetect" and mem.initstate == INITSTATE_HWDETECT_RTC then | |
mem.initstate = INITSTATE_HWDETECT_NIC | |
digiline_send("nic","http://clients1.google.com/generate_204") | |
interrupt(1,"nicdetect") | |
elseif mem.initstate == INITSTATE_HWDETECT_NIC and event.channel == "nic" then | |
mem.nicfound = true | |
mem.niccommand = false | |
elseif event.iid == "nicdetect" and mem.initstate == INITSTATE_HWDETECT_NIC then | |
mem.initstate = INITSTATE_SUMMARY | |
elseif mem.initstate == INITSTATE_READY and mem.blinkypin and (event.type == "on" or event.type == "off") then | |
if mem.nicfound and mem.url then digiline_send("nic",mem.url) end | |
if mem.lsfound then digiline_send("sensor","GET") end | |
if mem.rtcfound then digiline_send("rtc","GET") end | |
elseif mem.initstate == INITSTATE_READY and event.channel == "nic" then | |
mem.niccommand = string.sub(event.msg.data,1,2) == "on" | |
elseif mem.initstate == INITSTATE_READY and event.channel == "sensor" then | |
mem.lightlevel = event.msg | |
elseif mem.initstate == INITSTATE_READY and event.channel == "rtc" then | |
mem.gametime = event.msg | |
end | |
if mem.initstate == INITSTATE_READY then | |
local desstate = getdesiredstate() | |
port = {} | |
for _,i in ipairs({"a","b","c","d"}) do | |
port[i] = desstate and ((not mem.blinkypin) or string.lower(mem.blinkypin) ~= i) | |
end | |
digiline_send("light",desstate and mem.brightness or mem.offbrightness) | |
mem.lightstate = desstate | |
else | |
port = {} | |
digiline_send("light",0) | |
mem.lightstate = false | |
end | |
if mem.screenstate == SCREENSTATE_INIT then | |
fsadd("label[0,0;Hardware Setup Wizard]") | |
if mem.initstate == INITSTATE_POWERON then | |
fsadd("label[0,1;Thank you for choosing Light Controller 2]") | |
fsadd("label[0,1.5;by Advanced Mesecons Devices\\, a Cheapie Systems company.]") | |
fsadd("label[0,3;This wizard will help you configure your new light controller.]") | |
fsadd("label[0,3.5;Click next to continue.]") | |
fsadd("image_button[11,7;1,1;digistuff_adwaita_go-next.png;next;]") | |
elseif mem.initstate == INITSTATE_HWDETECT_BLINKY then | |
fsadd("label[0,1;Detecting hardware, please wait...]") | |
fsadd("label[0,1.5;Blinky Plant]") | |
elseif mem.initstate == INITSTATE_HWDETECT_LS then | |
fsadd("label[0,1;Detecting hardware, please wait...]") | |
fsadd("label[0,1.5;Light Sensor]") | |
elseif mem.initstate == INITSTATE_HWDETECT_RTC then | |
fsadd("label[0,1;Detecting hardware, please wait...]") | |
fsadd("label[0,1.5;Real Time Clock]") | |
elseif mem.initstate == INITSTATE_HWDETECT_NIC then | |
fsadd("label[0,1;Detecting hardware, please wait...]") | |
fsadd("label[0,1.5;NIC]") | |
elseif mem.initstate == INITSTATE_SUMMARY then | |
fsadd("label[0,1;The following hardware was detected\\:]") | |
fsadd("label[0,2;Blinky Plant: "..(mem.blinkypin and ("Yes\\, pin "..mem.blinkypin.."]") or "No]")) | |
fsadd("label[0,2.5;Light Sensor (channel 'sensor'): "..(mem.lsfound and "Yes" or "No").."]") | |
fsadd("label[0,3;Real Time Clock (channel 'rtc'): "..(mem.rtcfound and "Yes" or "No").."]") | |
fsadd("label[0,3.5;NIC (channel 'nic'): "..(mem.nicfound and "Yes" or "No").."]") | |
fsadd("label[0,6;If this looks correct\\, press next.]") | |
fsadd("label[0,6.5;Otherwise\\, press back to try again.]") | |
local usablemodes = {"Always Off","Always On"} | |
local unusablemodes = {} | |
if mem.blinkypin then | |
table.insert(usablemodes,"Real-Life Time") | |
if mem.lsfound then | |
table.insert(usablemodes,"Dusk to Dawn") | |
table.insert(usablemodes,"Dawn to Dusk") | |
else | |
table.insert(unusablemodes,"Dusk to Dawn (light sensor)") | |
table.insert(unusablemodes,"Dawn to Dusk (light sensor)") | |
end | |
if mem.rtcfound then | |
table.insert(usablemodes,"In-Game Time") | |
else | |
table.insert(unusablemodes,"In-Game Time (RTC)") | |
end | |
if mem.nicfound then | |
table.insert(usablemodes,"HTTP Remote") | |
else | |
table.insert(unusablemodes,"HTTP Remote (NIC)") | |
end | |
else | |
table.insert(unusablemodes,"All Automatic Modes (blinky plant)") | |
end | |
local usablemodestr = "" | |
for _,i in ipairs(usablemodes) do usablemodestr = usablemodestr..i.."\\, " end | |
usablemodestr = string.sub(usablemodestr,1,-4) | |
local unusablemodestr = "" | |
for _,i in ipairs(unusablemodes) do unusablemodestr = unusablemodestr..i.."\\, " end | |
unusablemodestr = string.sub(unusablemodestr,1,-4) | |
if #unusablemodes == 0 then | |
usablemodestr = "All Modes" | |
unusablemodestr = "None" | |
end | |
fsadd("label[0,4.5;Usable Modes\\: "..usablemodestr.."]") | |
fsadd("label[0,5;Modes Missing Hardware\\: "..unusablemodestr.."]") | |
fsadd("image_button[11,7;1,1;digistuff_adwaita_go-next.png;next;]") | |
fsadd("image_button[10,7;1,1;digistuff_adwaita_go-previous.png;back;]") | |
end | |
elseif mem.screenstate == SCREENSTATE_MAIN then | |
fsadd("label[0,0;Main Menu]") | |
fsadd(string.format("label[0,1;Lights are currently\\: %s]",mem.lightstate and "ON" or "OFF")) | |
fsadd(string.format("label[0,1.5;Mode\\: %s]",describemode())) | |
fsadd("button[0,2.5;2,1;viewstatus;View Status]") | |
fsadd("button[0,3.5;2,1;selectmode;Select Mode]") | |
fsadd("button[0,4.5;2,1;editsettings;Edit Settings]") | |
fsadd("button[0,7;2,1;about;About]") | |
elseif mem.screenstate == SCREENSTATE_STATUS then | |
fsadd("label[0,0;System Status]") | |
fsadd(string.format("label[0,1;Mode\\: %s]",modestrings[mem.mode])) | |
fsadd(string.format("label[0,1.5;Output\\: %s]",mem.lightstate and "ON" or "OFF")) | |
local lpos = 2 | |
if mem.lsfound and mem.blinkypin then | |
fsadd(string.format("label[0,%1.1f;Light level\\: %s]",lpos,mem.lightlevel)) | |
lpos = lpos + 0.5 | |
end | |
if mem.rtcfound and mem.blinkypin then | |
local gametime = formatgametime(mem.gametime) | |
fsadd(string.format("label[0,%1.1f;In-game time\\: %02d\\:%02d]",lpos,gametime.hour,gametime.min)) | |
lpos = lpos + 0.5 | |
end | |
if mem.nicfound and mem.url and mem.blinkypin then | |
fsadd(string.format("label[0,%1.1f;HTTP remote command\\: %s]",lpos,mem.niccommand and "ON" or "OFF")) | |
lpos = lpos + 0.5 | |
end | |
local datetable = os.datetable() | |
if mem.blinkypin then | |
fsadd(string.format("label[0,%1.1f;Real-life time\\: %02d\\:%02d]",lpos,datetable.hour,datetable.min)) | |
end | |
fsadd("button[0,7;2,1;back;Main Menu]") | |
elseif mem.screenstate == SCREENSTATE_ABOUT then | |
fsadd("label[0,0;About]") | |
fsadd("label[0,1;Light Controller 2 by Advanced Mesecons Devices\\,]") | |
fsadd("label[0,1.5;a Cheapie Systems company.]") | |
fsadd("label[0,2.5;This is free and unencumbered software released into the public domain.]") | |
fsadd("label[0,3;See https\\://unlicense.org/ for the full license text.]") | |
fsadd("button[0,7;2,1;back;Main Menu]") | |
elseif mem.screenstate == SCREENSTATE_SETTINGS then | |
fsadd("label[0,0;Edit Settings]") | |
fsadd("button[0,7;2,1;save;Save]") | |
fsadd("button[2,7;2,1;cancel;Cancel]") | |
fsadd("button[10,7;2,1;defaults;Restore Defaults]") | |
fsadd("label[0,0.75;Times (for game time and real-life time modes)]") | |
fsadd(string.format("field[0.25,1.5;0.75,1;onhour;;%02d]",mem.ontime.hour)) | |
fsadd("label[0.65,1.4;\\:]") | |
fsadd(string.format("field[1.15,1.5;0.75,1;onmin;;%02d]",mem.ontime.min)) | |
fsadd("label[1.55,1.4;—]") | |
fsadd(string.format("field[2.15,1.5;0.75,1;offhour;;%02d]",mem.offtime.hour)) | |
fsadd("label[2.55,1.4;\\:]") | |
fsadd(string.format("field[3.05,1.5;0.75,1;offmin;;%02d]",mem.offtime.min)) | |
fsadd(string.format("field[0.25,3;2,1;triggerlevel;Trigger Level;%d]",mem.lighttrig)) | |
fsadd(string.format("field[0.25,4;2,1;brightness;Brightness (on);%d]",mem.brightness)) | |
fsadd(string.format("field[0.25,5;2,1;offbrightness;Brightness (off);%d]",mem.offbrightness)) | |
local urlesc = "" | |
if mem.url then | |
local badchars = { | |
["\\"] = true, | |
["["] = true, | |
["]"] = true, | |
[";"] = true, | |
[","] = true | |
} | |
for i=1,string.len(mem.url),1 do | |
local char = string.sub(mem.url,i,i) | |
if badchars[char] then urlesc = urlesc.."\\" end | |
urlesc = urlesc..char | |
end | |
else | |
urlesc = "" | |
end | |
fsadd(string.format("field[0.25,6;4,1;url;Remote Control URL;%s]",urlesc)) | |
elseif mem.screenstate == SCREENSTATE_MODE then | |
fsadd("label[0,0;Select Mode]") | |
fsadd("button[0,7;2,1;back;Main Menu]") | |
local modes = {"Always Off","Always On"} | |
if mem.blinkypin then | |
if mem.lsfound then | |
table.insert(modes,"Dusk to Dawn") | |
table.insert(modes,"Dawn to Dusk") | |
end | |
if mem.rtcfound then | |
table.insert(modes,"In-Game Time") | |
end | |
table.insert(modes,"Real-Life Time") | |
if mem.nicfound then | |
table.insert(modes,"HTTP Remote") | |
end | |
end | |
local modesrev = {} | |
for k,v in ipairs(modes) do modesrev[v] = k end | |
local sel = 0 | |
if mem.mode == MODE_OFF then sel = modesrev["Always Off"] | |
elseif mem.mode == MODE_ON then sel = modesrev["Always On"] | |
elseif mem.mode == MODE_DUSKDAWN then sel = modesrev["Dusk to Dawn"] | |
elseif mem.mode == MODE_DAWNDUSK then sel = modesrev["Dawn to Dusk"] | |
elseif mem.mode == MODE_GAMETIME then sel = modesrev["In-Game Time"] | |
elseif mem.mode == MODE_REALTIME then sel = modesrev["Real-Life Time"] | |
elseif mem.mode == MODE_INTERNET then sel = modesrev["HTTP Remote"] end | |
if not sel then sel = 1 end | |
local modestr = "" | |
for _,i in pairs(modes) do | |
modestr = modestr..i.."," | |
end | |
modestr = string.sub(modestr,1,-2) | |
fsadd(string.format("textlist[0.25,1;6,6;mode;%s;%d;false]",modestr,sel)) | |
fsadd(string.format("label[7,1;%s]",modestrings[mem.mode])) | |
if mem.mode == MODE_OFF then | |
fsadd("label[7,2;Output is always off.]") | |
elseif mem.mode == MODE_ON then | |
fsadd("label[7,2;Output is always on.]") | |
elseif mem.mode == MODE_DUSKDAWN then | |
fsadd("label[7,2;Output is on when the light level]") | |
fsadd("label[7,2.5;is below the set threshold.]") | |
elseif mem.mode == MODE_DAWNDUSK then | |
fsadd("label[7,2;Output is on when the light level]") | |
fsadd("label[7,2.5;is above the set threshold.]") | |
elseif mem.mode == MODE_GAMETIME then | |
fsadd("label[7,2;Output turns on at the set on time]") | |
fsadd("label[7,2.5;and off at the set off time.]") | |
fsadd("label[7,3;Uses in-game time.]") | |
elseif mem.mode == MODE_REALTIME then | |
fsadd("label[7,2;Output turns on at the set on time]") | |
fsadd("label[7,2.5;and off at the set off time.]") | |
fsadd("label[7,3;Uses real-life time.]") | |
elseif mem.mode == MODE_INTERNET then | |
fsadd("label[7,2;Output is on when the file at the]") | |
fsadd("label[7,2.5;specified URL starts with \"on\".]") | |
end | |
end | |
digiline_send("touchscreen",fs) |
It uses the advanced touchscreen (not the standard one), but yes. It also expects a blinky plant on any pin.
The RTC, light sensor, NIC, and blinky plant are all technically optional (it detects what is connected during startup), but functionality is limited if not all of them are present:
- Always On and Always Off modes don't need any extra parts
- Real-Life Time mode requires a blinky plant
- In-Game Time mode requires a blinky plant and an RTC
- Dusk to Dawn and Dawn to Dusk modes require a blinky plant and a light sensor
- HTTP Remote mode requires a blinky plant and a NIC
Mesecons output is also supported, output is on every pin except the pin the blinky plant is connected to.
Homedecor lights (glowlights and such) can also be controlled over digilines, and the protocol is compatible with the ones from digistuff.
As mentioned both at the start of the program and on the about screen, you can do anything you want with this program, and you don't have to give me credit for it either. Credit is, however, of course appreciated.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice 👍
Judging from the code it needs these devices, right?
rtc
sensor
touchscreen
nic
light
I'd like to add stuff like this in my
mesecons_lab
game https://content.minetest.net/packages/BuckarooBanzay/mesecons_lab/ (tutorial/examples game) let me know if thats alright with you (i'll credit you accordingly)