Skip to content

Instantly share code, notes, and snippets.

@Fingercomp
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