Skip to content

Instantly share code, notes, and snippets.

@Fingercomp Fingercomp/launcher.lua Secret forked from anonymous/launcher.lua
Last active Feb 6, 2016

Embed
What would you like to do?
OpenComputers Game Launcher
local DEBUG = {}
local LOG = "/var/log/launcher.log"
local GAMESDIR = "/games/"
local W, H = 80, 40
local OWNER = "Fingercomp"
local NOINTERRUPT = true
-- OpenOS Libs
local com = require("component")
local event = require("event")
local fs = require("filesystem")
local inet = require("internet")
local term = require("term")
local unicode = require("unicode")
local comp = require("computer")
local kbd = require("keyboard")
-- Components
local inetcom = com.internet
local gpu = com.gpu
-- Dynamic libs
local function loadLib(libPath, url, exec)
if not fs.exists(libPath) then
local f = io.open(libPath, "w")
local response = inet.request(url)
for chunk in response do
f:write(chunk)
f:flush()
end
f:close()
end
if not exec then
return loadfile(libPath)()
else
os.execute(libPath)
end
end
if not fs.exists("/lib/doubleBuffering.lua") then
loadLib("/tmp/buffer.lua", "http://pastebin.com/raw/vTM8nbSZ", true)
end
local buffer = require("doubleBuffering")
local json = loadLib("/usr/lib/json.lua", "http://regex.info/code/JSON.lua")
local image = require("image")
-- Program variables
local w, h
local games = {}
local mode = {"main"}
local dbg, log, redrawMode, save, trunc, getRating
local noExit = true
local popular = {}
local liked = {}
local rating = {}
local searchList = {}
-- Pre-actions
for game in fs.list(GAMESDIR) do
if fs.exists(fs.concat(GAMESDIR, game, "info")) then
local key = game:sub(-1, -1, "/") and game:sub(1, -2) or game
local f = io.open(fs.concat(GAMESDIR, game, "info"))
games[key] = json:decode(f:read("*a"))
f:close()
end
end
gpu.setResolution(W, H)
w, h = gpu.getResolution()
if NOINTERRUPT then
event.shouldInterrupt = function()
return false
end
end
if com.isAvailable("redstone") then
com.redstone.setWakeThreshold(1)
end
-- Functions
local function tblen(tbl)
local max = 0
for num, _ in pairs(tbl) do
max = max + 1
end
return max
end
local function maxn(tbl)
local max = 0
for num, _ in pairs(tbl) do
max = math.max(max, num)
end
return max
end
local function isin(tbl, value)
for num, i in pairs(tbl) do
if i == value then
return true, num
end
end
return false
end
local function str2hex(color)
local hextbl = {[0]="0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"}
local result = 0
for i = color:len(), 1, -1 do
local _, pos = isin(hextbl, color:sub(i, i))
dbg("str2hex", color:sub(i, i) .. ", " .. pos .. ", " .. pos * 16^(color:len() - i - 1) .. " | " .. result)
result = result + pos * 16^(color:len() - i)
end
dbg("str2hex", result)
return result
end
local function center(text, width)
width = width or w
return math.floor(width / 2 - unicode.len(text) / 2)
end
local function justifyText(text, width)
local result = {}
for i = 1, unicode.len(text), width do
dbg("justify", i .. "/" .. unicode.len(text) .. " @ " .. width .. " (" .. i .. ":" .. i + width - 1 .. ")")
table.insert(result, unicode.sub(text, i, i + width - 1))
dbg("justify", unicode.sub(text, i, i + width - 1))
end
return result
end
dbg = function(section, msg)
if isin(DEBUG, section) or isin(DEBUG, "all") then
local f = io.open(LOG, "a")
f:write("[" .. os.date() .. "] [DBG] [" .. section .. "] " .. msg .. "\n")
f:close()
end
end
log = function(msg)
local f = io.open(LOG, "a")
f:write("[" .. os.date() .. "] " .. msg .. "\n")
f:close()
end
local function percRes(axis, perc)
axis = axis == "x" and w or h
return math.floor(axis / 100 * perc)
end
local function download(url, path)
local response = inet.request(url)
if not response then
log("Загрузка " .. url .. " в " .. path .. " завершена с ошибкой: response = nil")
return false
end
local f = io.open(path, "w")
for chunk in response do
f:write(chunk)
end
f:close()
return true
end
-- Copypasted from https://gist.github.com/Nayruden/427389
local function editDistance(s, t, lim)
local s_len, t_len = #s, #t -- Calculate the sizes of the strings or arrays
if lim and math.abs(s_len - t_len) >= lim then -- If sizes differ by lim, we can stop here
return lim
end
-- Convert string arguments to arrays of ints (ASCII values)
if type(s) == "string" then
s = {string.byte(s, 1, s_len)}
end
if type(t) == "string" then
t = {string.byte(t, 1, t_len)}
end
local min = math.min -- Localize for performance
local num_columns = t_len + 1 -- We use this a lot
local d = {}
for i = 0, s_len do
d[i * num_columns] = i -- Initialize cost of deletion
end
for j = 0, t_len do
d[j] = j -- Initialize cost of insertion
end
for i = 1, s_len do
local i_pos = i * num_columns
local best = lim -- Check to make sure something in this row will be below the limit
for j = 1, t_len do
local add_cost = (s[i] ~= t[j] and 1 or 0)
local val = min(d[i_pos - num_columns + j] + 1, d[i_pos + j - 1] + 1, d[i_pos - num_columns + j - 1] + add_cost)
d[i_pos + j] = val
-- Is this eligible for tranposition?
if i > 1 and j > 1 and s[i] == t[j - 1] and s[i - 1] == t[j] then
d[i_pos + j] = min(val, d[i_pos - num_columns - num_columns + j - 2] + add_cost)
end
if lim and val < best then
best = val
end
end
if lim and best >= lim then
return lim
end
end
return d[#d]
end
local function search(tbl, value, maxResults)
maxResults = maxResults or math.huge
local subs = {}
local dists = {}
for _, v in pairs(tbl) do
local name = games[v].name
if unicode.sub(unicode.lower(name), 1, unicode.len(value)) == unicode.lower(value) then
table.insert(subs, v)
end
local dist = editDistance(unicode.lower(name), unicode.lower(value))
dists[dist] = dists[dist] or {}
table.insert(dists[dist], v)
end
local results = {}
for i = 1, #subs, 1 do
if #results == maxResults then
return results
end
table.insert(results, subs[i])
end
for i = 0, math.min(maxn(dists), 5), 1 do
if dists[i] then
for _, game in pairs(dists[i]) do
if #results == maxResults then
return results
end
if not isin(results, game) then
table.insert(results, game)
end
end
end
end
return results
end
local function showMenu(title, bg, fg)
buffer.square(1, 1, w, h, 0xffffff, 0x000000, " ")
buffer.square(1, 1, w, 3, bg, fg, " ")
buffer.square(1, 1, 3, 3, 0x000000, fg, " ", 90)
buffer.text(2, 2, fg, "")
buffer.text(5, 2, fg, title)
buffer.square(w - 6, 1, 7, 3, 0x000000, fg, " ", 90)
buffer.text(w - 5, 2, fg, "Поиск")
buffer.draw()
end
local function getMostPopular()
local mostPop = {"", -1}
for k,v in pairs(games) do
if v.stats.played > mostPop[2] then
mostPop = {k, v.stats.played}
end
end
return table.unpack(mostPop)
end
local function getMostLiked()
local mostLiked = {"", -1}
for k,v in pairs(games) do
if #v.stats.likes > mostLiked[2] then
mostLiked = {k, #v.stats.likes}
end
end
return table.unpack(mostLiked)
end
local function getHighestRating()
local highRating = {"", -1}
for k, v in pairs(games) do
local rating = 0
for _, rateVal in pairs(v.stats.rate) do
rating = rating + rateVal
end
if rating > highRating[2] then
highRating = {k, rating}
end
end
return table.unpack(highRating)
end
local function drawSuggestions()
rating = {getHighestRating()}
popular = {getMostPopular()}
liked = {getMostLiked()}
buffer.text(center("Воспользуйтесь поиском или выберите из предложенного"), 6, 0x000000, "Воспользуйтесь поиском или выберите из предложенного")
buffer.square(3, 12, 24, 12, 0x108010, 0xffffff, " ")
buffer.square(29, 12, 24, 12, 0x20afff, 0xffffff, " ")
buffer.square(55, 12, 24, 12, 0xff4380, 0xffffff, " ")
buffer.text(5, 14, 0xffffff, "ЛУЧШЕЕ")
buffer.text(31, 14, 0xffffff, "ПОПУЛЯРНОЕ")
buffer.text(57, 14, 0xffffff, "ПОНРАВИВШЕЕСЯ")
buffer.text(5, 22, 0xffffff, rating[1])
buffer.text(31, 22, 0xffffff, popular[1])
buffer.text(57, 22, 0xffffff, liked[1])
buffer.draw()
end
local function drawGameInfo(game, bg, fg)
buffer.square(w - 14, 1, 6, 3, 0x0, 0x0, " ", 90)
buffer.square(w - 21, 1, 6, 3, 0x0, 0x0, " ", 90)
buffer.square(w - 25, 1, 3, 3, 0x0, 0x0, " ", 90)
buffer.square(w - 34, 1, 3, 3, 0x0, 0x0, " ", 90)
buffer.text(w - 33, 2, fg, "-")
buffer.text(w - 31 + center(trunc(getRating(game, true), "+"):len(), 6), 2, fg, trunc(getRating(game, true), "+"))
buffer.text(w - 24, 2, fg, "+")
buffer.text(w - 21 + center(trunc(#games[game].stats.likes):len(), 6), 2, fg, "" .. trunc(#games[game].stats.likes))
buffer.text(w - 14 + center(trunc(games[game].stats.played):len(), 6), 2, fg, "" .. trunc(games[game].stats.played))
local justifiedText = justifyText(games[game].description, w - 9)
buffer.square(w - 8, 6, 8, 1, bg, fg, " ")
buffer.text(w - 7, 6, fg, "Играть")
buffer.text(3, 6, 0x0, "Информация об игре \"" .. games[game].name .. "\" [" .. game .. "]")
buffer.text(3, 9, 0x0, "Автор: " .. games[game].author)
buffer.text(3, 10, 0x0, "Версия: " .. games[game].version)
buffer.text(3, 11, 0x0, "Режим: " .. (games[game].mp and "многопользовательский" or "одиночный"))
buffer.text(3, 13, 0x0, "Описание:")
for line, i in pairs(justifiedText) do
dbg("justified", line .. ": " .. i)
buffer.text(6, 13 + line, 0x0, i)
end
buffer.draw()
end
local function setRating(game, user, rate)
if games[game].stats.rate[user] and games[game].stats.rate[user] == rate then
games[game].stats.rate[user] = nil
else
games[game].stats.rate[user] = rate
end
save()
redrawMode()
end
local function like(game, user)
local likedAlready, pos = isin(games[game].stats.likes, user)
if likedAlready then
table.remove(games[game].stats.likes, pos)
else
table.insert(games[game].stats.likes, user)
end
save()
redrawMode()
end
local function incPldCtr(game, user)
games[game].stats.played = games[game].stats.played + 1
games[game].stats.players[user] = (games[game].stats.players[user] or 0) + 1
save()
end
local function play(game, user)
log("Запрошен запуск игры " .. game .. " игроком " .. user)
local path = fs.concat(GAMESDIR, game, games[game].runfile)
log("Путь к runfile: " .. path)
buffer.square(1, 1, w, h, 0x000000, 0xffffff, " ")
buffer.draw()
term.setCursor(1, 1)
io.write("Проверка файла... ")
if not fs.exists(path) then
log("Файл отсутствует, скачивание")
print("Файл не существует!")
io.write("Скачивание... ")
if not games[game].link then
log("Не указан URL, завершение")
print("Не указан URL!")
print("[Нажмите любую клавишу]")
event.pull(30, "key_down")
redrawMode()
return false
end
local dlRslt = {pcall(download, games[game].link, path)}
if dlRslt[1] then
print("OK")
else
print("Ошибка")
log("Ошибка скачивания " .. games[game].link)
if dlRslt[2] then
log(dlRslt[2])
end
fs.remove(path)
print("[Нажмите любую клавишу]")
event.pull(30, "key_down")
redrawMode()
return false
end
else
print("OK")
end
io.write("Сборка байт-кода... ")
local suc, reason = loadfile(path, _, _, _ENV)
if not suc then
print("Ошибка! Причина записана в логе.")
log("Ошибка при сборке файла " .. path .. " [" .. game .. "]")
log(reason)
print("[Нажмите любую клавишу]")
event.pull(30, "key_down")
redrawMode()
return false
else
print("OK")
io.write("Запуск игры... ")
if not games[game].mp then comp.addUser(user) end
local success, rsn = xpcall(suc, debug.traceback)
if not success then
comp.removeUser(user)
print("Ошибка! Причина записана в логе.")
log("Ошибка при исполнении файла " .. path .. " [" .. game .. "]")
log(rsn)
print("[Нажмите любую клавишу]")
event.pull(30, "key_down")
redrawMode()
return false
else
comp.removeUser(user)
gpu.setResolution(w, h)
gpu.setForeground(0xffffff)
log("Сессия завершена, инкремент счётчика")
buffer.square(1, 1, w, h, 0x101010, 0xffffff, " ")
buffer.draw()
buffer.square(1, 1, w, h, 0x000000, 0xffffff, " ")
buffer.draw()
term.setCursor(1, 1)
print("Сессия завершена")
incPldCtr(game, user)
end
end
print("[Нажмите любую клавишу]")
event.pull(30, "key_down")
redrawMode()
save()
end
local function drawSearch(active, noHint)
local color = active and 0x606060 or 0xc3c3c3
buffer.text(3, 6, color, "" .. (""):rep(w - 8) .. "")
buffer.text(3, 7, color, "" .. (" "):rep(w - 8) .. "")
buffer.text(3, 8, color, "" .. (""):rep(w - 8) .. "")
if not noHint then buffer.text(3, 10, 0x000000, "Воспользуйтесь поиском сверху или выберите игру из списка") end
buffer.text(4, 7, 0x0, unicode.sub(mode[3] or "", mode[5], mode[5] + w - 9))
buffer.draw()
end
trunc = function(num, plus, zeroSign)
plus = plus or ""
local sign = zeroSign or ""
if num < 0 then
sign = "-"
elseif num > 0 then
sign = plus
end
num = math.abs(num)
if num < 10^4 then
return sign .. num
elseif num >= 10^4 and num < 10^5 then
return sign .. math.floor(num / 10^2) / 10 .. "k"
elseif num >= 10^5 and num < 10^6 then
return sign .. math.floor(num / 10^3) .. "k"
elseif num >= 10^6 and num < 10^7 then
return sign .. math.floor(num / 10^4) / 100 .. "M"
elseif num >= 10^7 and num < 10^8 then
return sign .. math.floor(num / 10^50) / 10 .. "M"
elseif num >= 10^8 and num < 10^9 then
return sign .. math.floor(num / 10^6) .. "M"
end
end
getRating = function(game, noTrunc)
local rate = 0
for _, value in pairs(games[game].stats.rate) do
rate = rate + value
end
dbg('rating', rate)
if noTrunc then return rate end
trRate = trunc(rate, "+", " ")
return trRate
end
local function drawGamesList(gmtbl, startLine)
searchList = gmtbl
local listHeight = w - startLine
local items2show = math.ceil(listHeight / 3)
local truncate = listHeight - items2show * 3
for i = 1, items2show, 1 do
if gmtbl[i] then
local opacity = i % 2 == 1 and 90 or 80
local rateColor = 0x000000
if getRating(gmtbl[i], true) > 0 then
rateColor = 0x00ff00
elseif getRating(gmtbl[i], true) < 0 then
rateColor = 0xff0000
end
buffer.square(1, startLine + (i - 1) * 3, w, 3, 0x000000, 0x000000, " ", opacity)
buffer.square(1, startLine + (i - 1) * 3, 2, 3, str2hex(games[gmtbl[i]].color), 0x000000, " ")
buffer.text(4, startLine + (i - 1) * 3 + 1, 0x000000, games[gmtbl[i]].name)
buffer.square(w - 10, startLine + (i - 1) * 3, 9, 3, 0x000000, 0x000000, " ", 90)
buffer.text(w - 9, startLine + (i - 1) * 3, 0xe8a0a0, "" .. trunc(#games[gmtbl[i]].stats.likes))
buffer.text(w - 9, startLine + (i - 1) * 3 + 1, rateColor, " " .. getRating(gmtbl[i]))
buffer.text(w - 9, startLine + (i - 1) * 3 + 2, 0x008000, "" .. trunc(games[gmtbl[i]].stats.played))
end
end
buffer.draw()
end
local function getGamesList()
local result = {}
for k, _ in pairs(games) do
table.insert(result, k)
end
return result
end
redrawMode = function()
if mode[1] == "main" then
showMenu("ГЛАВНОЕ МЕНЮ", 0xf0f0f0, 0x000000)
drawSuggestions()
elseif mode[1] == "search" then
showMenu("ПОИСК", 0x10afff, 0xffffff)
if not mode[3] or mode[3] == "" then
drawSearch(mode[2])
drawGamesList(getGamesList(), 12)
else
drawSearch(mode[2], true)
local results = search(getGamesList(), mode[3])
drawGamesList(results, 10)
end
elseif mode[1] == "info" then
showMenu(unicode.upper(games[mode[2]].name), str2hex(games[mode[2]].color), str2hex(games[mode[2]].text))
drawGameInfo(mode[2], str2hex(games[mode[2]].color), str2hex(games[mode[2]].text))
end
end
local function isInBox(x, y, w, h, a, b)
if a >= x and a <= x + w and b >= y and b <= y + h then
return true
end
return false
end
local function touchHandle(data)
local x, y = data[3], data[4]
if isInBox(1, 1, 3, 3, x, y) and mode[1] ~= "main" then
mode = {"main"}
redrawMode()
return
end
if isInBox(w - 7, 1, 7, 3, x, y) then
mode = {"search", false, "", 1, 1}
redrawMode()
return
end
if mode[1] == "main" then
if isInBox(3, 12, 24, 12, x, y) then
-- Rating
mode = {"info", rating[1]}
redrawMode()
elseif isInBox(29, 12, 24, 12, x, y) then
-- Popular
mode = {"info", popular[1]}
redrawMode()
elseif isInBox(55, 12, 24, 12, x, y) then
-- Liked
mode = {"info", liked[1]}
redrawMode()
elseif isInBox(1, 1, 3, 3, x, y) and data[6] == OWNER then
noExit = false
end
elseif mode[1] == "info" then
if isInBox(w - 8, 6, 8, 1, x, y) then
play(mode[2], data[6])
elseif isInBox(w - 36, 1, 3, 3, x, y) then
setRating(mode[2], data[6], -1)
elseif isInBox(w - 25, 1, 3, 3, x, y) then
setRating(mode[2], data[6], 1)
elseif isInBox(w - 24, 1, 6, 3, x, y) then
like(mode[2], data[6])
end
elseif mode[1] == "search" then
local yOffset = 10
if not mode[3] or mode[3] == "" then
yOffset = 12
end
local gamePos = math.floor((y - yOffset) / 3 + 1)
if gamePos > 0 and searchList[gamePos] then
mode = {"info", searchList[gamePos]}
redrawMode()
end
if isInBox(5, 6, w - 10, 3, x, y) then
mode = {"search", not mode[2], (mode[3] or ""), mode[4] or 1, mode[5] or 1}
redrawMode()
end
end
end
local function setCursor(pos, noRedraw)
if mode[4] + pos < 1 then
mode[4] = 1
mode[5] = 1
return
elseif mode[4] + pos > unicode.len(mode[3]) + 1 then
mode[4] = unicode.len(mode[3]) + 1
mode[5] = mode[4]
return
elseif mode[4] + pos < mode[5] then
mode[4] = mode[4] + pos
mode[5] = mode[4]
elseif mode[4] + pos > mode[5] + w - 8 then
mode[4] = mode[4] + pos
mode[5] = mode[4] + 8 - w
else
mode[4] = mode[4] + pos
end
if not noRedraw then redrawMode() end
end
local function keyHandle(data)
if mode[1] == "search" and mode[2] then
mode[3] = mode[3] or ""
if data[4] == 203 then -- Left
setCursor(-1)
elseif data[4] == 205 then -- Right
setCursor(1)
elseif data[4] == 28 then -- Enter
mode = {"search", false, mode[3], mode[4], mode[5]}
redrawMode()
elseif data[4] == 14 then -- Backspace
if mode[4] > 1 then
mode[3] = unicode.sub(mode[3], 1, mode[4] - 2) .. unicode.sub(mode[3], mode[4], -1)
setCursor(-1)
end
elseif data[4] == 211 then -- Delete
if mode[4] <= unicode.len(mode[3]) then
mode[3] = unicode.sub(mode[3], 1, mode[4] - 1) .. unicode.sub(mode[3], mode[4] + 1, -1)
end
elseif data[4] == 207 then -- End
setCursor(unicode.len(mode[3]) - mode[4] + 1)
elseif data[4] == 199 then -- Home
setCursor(-mode[4] + 1)
elseif not isin({54, 201, 209, 210, 200, 208}, data[4]) then
-- Shift, PgUp, PgDn, Ins, Up, Down
if data[3] ~= 0 then
local char = kbd.isShiftDown() and unicode.upper(unicode.char(data[3])) or unicode.char(data[3])
mode[3] = unicode.sub(mode[3], 1, mode[4] - 1) .. char .. unicode.sub(mode[3], mode[4], -1)
setCursor(1)
end
end
end
end
save = function()
log("Запрошено сохранение")
for game, value in pairs(games) do
local data = json:encode_pretty(value) -- Just 'cause I can ;P
local f = io.open(fs.concat(GAMESDIR, game, "info"), "w")
f:write(data)
f:close()
end
log("Сохранено")
end
local function quit()
save()
buffer.square(1, 1, w, h, 0x000000, 0xffffff, " ")
buffer.draw()
term.setCursor(1, 1)
end
local function main()
while noExit do
local data = {event.pull()}
if data then
if data[1] == "key_down" then
keyHandle(data)
elseif data[1] == "touch" then
touchHandle(data)
end
end
end
end
-- Main
log("---------------")
log("Program started")
redrawMode()
main()
quit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.