diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81b0c56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dev +build/ +games/* +!games/.gitkeep + diff --git a/lib/chroot.lua b/lib/chroot.lua new file mode 100644 index 0000000..f0abb42 --- /dev/null +++ b/lib/chroot.lua @@ -0,0 +1,37 @@ +return function(ll) + +ll.mdir = nil +ll.mgme = nil + +local ffi = require 'ffi' +ffi.cdef [[ + int PHYSFS_mount(const char *, const char *, int); + int PHYSFS_unmount(const char *); +]] + +local baseReq = '?.lua;?/init.lua;' +function ll.mount(gme) + local mdir = gme.base .. gme.dir + ll.mgme = gme + + love.filesystem.setRequirePath('' + .. mdir .. '/?.lua;' + .. mdir .. '/?/init.lua;' + .. baseReq + ) + -- FIXME: Bug may appear in Linux. Recompile Love2D or use official PPA. + if ffi.C.PHYSFS_mount(mdir, '/', 0) == 0 + then error('Cannot mount '..mdir) + love.filesystem.setRequirePath(baseReq) + else ll.mdir = mdir + end +end + +function ll.umount() + if ll.mdir ~= nil then + ffi.C.PHYSFS_unmount(ll.mdir) + ll.mdir = nil + end +end + +end diff --git a/lib/fs.lua b/lib/fs.lua new file mode 100644 index 0000000..e6088c3 --- /dev/null +++ b/lib/fs.lua @@ -0,0 +1,20 @@ +return function(ll) + +function ll.fsIsAbs(f) + f = f:sub(1, 1) + return f == '/' or f == '\\' +end + +function ll.fsIsRel(f) + return not ll.fsIsAbs(f) +end + +function ll.fsFile(f) + return f:match '([^/\\]*)[/\\]*$' +end + +function ll.fsDir(f) + return f:match '^(.*)[/\\]+[^/\\]*[/\\]*$' +end + +end diff --git a/lib/game.lua b/lib/game.lua new file mode 100644 index 0000000..b9d88f7 --- /dev/null +++ b/lib/game.lua @@ -0,0 +1,100 @@ +return function(ll) + +ll.games = {} + +local function parse(conf) + local r = {} + local k, v + + local buf = '' + local t = 'str' + local esc = false + local prg = true + + local i = 1 + repeat + local ch = conf:sub(i, i) + + if ch == '\\' + then esc = true + + elseif ch == '#' then + repeat i = i + 1 + ch = conf:sub(i, i) + until ch == '' or ch == '\n' or ch == '\r' + + elseif ch == '[' then + buf = buf:match '%s*(.*)%s*' + assert(#buf == 0, 'Unexpected array usage') + t = 'arr' + prg = true + v = {} + + elseif ch == '' + or (ch == '=' and not k) + or (ch == ']' and t == 'arr') + or (ch == ';' and t == 'arr') + or ch == '\n' or ch == '\r' then + buf = buf:match '^%s*(.-)%s*$' + + if ch == '=' then + assert(t == 'str', 'Cannot use other types than string for key') + end + + if t == 'str' + or (t == 'arr' and ch == ']') + then prg = false end + + if not prg or ch == ';' then + if #buf ~= 0 then + if k then + if t == 'str' + then v = buf + elseif t == 'arr' + then table.insert(v, buf) + else error 'wut?' end + else k = buf + end + buf = '' + elseif ch ~= '' + and ch ~= '\r' + and ch ~= '\n' + then error 'empty buffer' + end + end + if k and v and not prg then + r[k] = v + k = nil + v = nil + buf = '' + t = 'str' + end + + elseif esc then + buf = buf .. ch + esc = false + + else buf = buf .. ch + end + i = i + 1 + until i >= #conf + 1 + return r +end + +function ll.gameNew(conf, file, base, dir) + local cfg = parse(conf or '') + local gme = { + name = cfg.name or dir or 'No name', + desc = cfg.desc or 'No description provided.', + base = base, + dir = dir, + main = cfg.main or 'main.lua', + screens = cfg.screens or cfg.pics or nil, + scrcur = 1, + scrprv = 1, + dat = nil, + } + return gme +end + +end diff --git a/lib/keyb.lua b/lib/keyb.lua new file mode 100644 index 0000000..e3c0719 --- /dev/null +++ b/lib/keyb.lua @@ -0,0 +1,81 @@ +return function(ll) + +local mx, my, mb, mpb +local dir, sc1, sc2, sclm + +-- d - direction (h, v, x, y, *) +-- c1 - coordinate before card (mouse) (be left or top) +-- c2 - coordinate after card (mouse) (be right or bottom) +-- clm - other coordinate limit (mouse) (set -1 to disable) +function ll.kbInit(d, c1, c2, clim) + if d == 'h' or d == 'v' or d == '*' + then dir = d + elseif d == 'y' + then dir = 'v' + elseif d == 'x' + then dir = 'h' + else error 'Direction must be *, h (x) or v (y)' + end + + c1, c2 = + tonumber(c1) or 0, + tonumber(c2) or 0 + sc1, sc2, sclm = + math.min(c1, c2), + math.max(c1, c2), + tonumber(clim) or -1 +end + +-- returns: <, >, o, m, nil +-- ^ and v if dir is * +function ll.kbGet() + assert(dir, 'Call ll.kbInit(dir, coord1, coord2, coordlimit) before') + mx, my = love.mouse.getPosition() + mpb = mb + if love.mouse.isDown(1) then mb = 1 + else mb = 0 + end + + if love.keyboard.isScancodeDown('up', 'w') + then return dir == '*' and '^' or '<' + + elseif love.keyboard.isScancodeDown('left', 'a') + then return '<' + + elseif love.keyboard.isScancodeDown('down', 's') + then return dir == '*' and 'v' or '>' + + elseif love.keyboard.isScancodeDown('right', 'd') + then return '>' + + elseif love.keyboard.isScancodeDown('return', 'space') + then return 'o' + + elseif love.keyboard.isDown 'menu' + then return 'm' + + elseif mb == 0 and mpb == 1 then -- unpressed + if dir == 'h' then + if sclm < 0 or my <= sclm then + if mx <= sc1 + then return '<' + elseif mx >= sc2 + then return '>' + else return 'o' + end + end + else + if sclm < 0 or mx <= sclm then + if my <= sc1 + then return '<' + elseif my >= sc2 + then return '>' + else return 'o' + end + end + end + + end +end + +end diff --git a/lib/load.lua b/lib/load.lua new file mode 100644 index 0000000..04a40e1 --- /dev/null +++ b/lib/load.lua @@ -0,0 +1,51 @@ +return function(ll) + +function ll.addGame(file, cont) + local dir = ll.fsDir(file) + file = ll.fsFile(file) + local ext = file:match '%.(%w+)$' + print(file, ext, dir) + return 'NO!', nil +end + +function ll.gameAdd(conf, file, base, dir) + local gme = ll.gameNew(conf, file, base, dir) + gme.dat = {} + + if gme.screens and gme.screens[1] then + gme.dat.scr = {} + for i = 1, #gme.screens do + table.insert(gme.dat.scr, love.graphics.newImage(ll.cfg.root .. gme.dir ..'/'.. gme.screens[i])) + end + end + + table.insert(ll.games, gme) + return gme +end + +local lfs = love.filesystem +local info = lfs.getInfo + +for _, dir in pairs(love.filesystem.getDirectoryItems(ll.cfg.root)) do + local isDir + if info + then isDir = info(ll.cfg.root .. dir).type == 'directory' + else isDir = lfs.isDirectory(ll.cfg.root .. dir) + end + + if isDir then + local file = ll.cfg.root .. dir..'/'.. 'info.ll' + local realDir = love.filesystem.getRealDirectory(file) + or love.filesystem.getRealDirectory(ll.cfg.root .. dir..'/main.lua') + if realDir + then ll.gameAdd( + love.filesystem.read(file), + file, + realDir ..'/'.. ll.cfg.root, + dir + ) + end + end +end + +end diff --git a/lib/main.lua b/lib/main.lua new file mode 100644 index 0000000..dee7506 --- /dev/null +++ b/lib/main.lua @@ -0,0 +1,27 @@ +local ll = {} + +ll.cfg = { + root = 'games/', +} + +require 'lib.fs' (ll) +require 'lib.game' (ll) +require 'lib.chroot' (ll) +require 'lib.load' (ll) +require 'lib.keyb' (ll) + +function ll.home() + ll.umount() + love.event.push('quit', 'restart') +end + +ll.dt = false +function ll.devtools() + if not ll.dt then + ll.dt = true + __LL = ll + pcall(function() require 'dev.tools' end) + end +end + +return ll diff --git a/ll-min.lua b/ll-min.lua index 37edda9..eafd105 100644 --- a/ll-min.lua +++ b/ll-min.lua @@ -1,8 +1,17 @@ --- minimal Love Loader API +-- Minimal Love Loader API +-- Version 2.1 +-- (c) Er2 2022 +-- Zlib License if not llUsed then COLDIV = love.getVersion() == 0 and 1 or 255 +MOBILE = love.system.getOS() == 'Android' + or love.system.getOS() == 'iOS' + +if MOBILE +then love.window.setFullscreen(true) +end function love.resize(x, y) W, H = x, y diff --git a/luapack.lua b/luapack.lua new file mode 100644 index 0000000..ab4dec7 --- /dev/null +++ b/luapack.lua @@ -0,0 +1,17 @@ +return { + entry = 'main.lua', + output = 'build/main.lua', + plug = { + require 'plug.minify' { + extGlob = { + 'love', + 'llUsed', + 'llHome', + 'resize', + 'COLDIV', + 'MOBILE', + 'W', 'H', + } + }, + } +} diff --git a/main.lua b/main.lua index c45271c..5dd9deb 100644 --- a/main.lua +++ b/main.lua @@ -1,219 +1,71 @@ +local ll = require 'lib.main' +error = love.errhand or love.errorhandler -local root = 'games/' - --- games library and selection -local gms = {} -local sel = 1 -local sgme - --- some internals -local cx, cy, cw, ch -local f, bf - -local ffi = require 'ffi' -local fd -ffi.cdef [[ - int PHYSFS_mount(const char *, const char *, int); - int PHYSFS_unmount(const char *); -]] - -local function llResz() - cw, ch = W / 1.25, H / 1.25 - cx, cy = (W - cw) / 2, (H - ch) / 2 - - local th = math.min(W, H) / 8 - - f = love.graphics.newFont(th / 3) - bf = love.graphics.newFont(th / 2) +function splash() + love.graphics.setColor(255, 255, 255, 100 / COLDIV) + if ll.mgme.screens and ll.mgme.screens[1] then + local img = love.graphics.newImage(ll.mgme.screens[1]) + love.graphics.draw(img, 0, 0, 0, W / img:getWidth(), H / img:getHeight()) + end + love.graphics.setColor(255, 255, 255, 255) + love.graphics.print('Loading '..ll.mgme.name, W / 2, H / 2) end -resize = llResz + +ll.skin = require 'skins.psp' (ll) require 'll-min' llUsed = true -local function chroot(dir, main) - fd = love.filesystem.getSource() .. dir - love.filesystem.setRequirePath( - '?.lua;?/init.lua;' - .. dir .. '/?.lua;' - .. dir .. '/?/init.lua;' - ) - -- FIXME: Bug in Linux (Debian) - ffi.C.PHYSFS_mount(fd, '/', 0); - if main then - load(love.filesystem.read(dir ..'/'.. main), main)() - end -end - -local function escChroot() - if fd then - ffi.C.PHYSFS_unmount(fd); - fd = nil - end -end - -local function gmeNew(cont, dir, file) - local gme = { - name = 'No name', - desc = 'No description provided.', - dir = dir, - main = 'main.lua', - pics = nil, - psel = 2, - ppsl = 1, - } - local fi = false - cont = cont or '' - cont = cont:gsub('[^\\]#[^\n]*', '') - for v in cont:gmatch '([^\n]+)' do - local k, v = v:match '^%s*([%w_]+)%s*=%s*(.*)%s*$' - if k == 'name' - or k == 'desc' - or k == 'main' - then gme[k] = v - fi = true - elseif k == 'pic' then - gme.pics = gme.pics or {} - table.insert(gme.pics, v) - fi = true - elseif k == 'pics' then - local t = {} - v = v:sub(2, -2) - for v in v:gmatch '%s*([^;]+%a+)%s*;?' - do table.insert(t, v) end - gme[k] = t - fi = true - elseif k - then error('unknown field "'.. k ..'" in "'.. file ..'"') - end - end - if gme.pics then - for k, v in pairs(gme.pics) do - gme.pics[k] = love.graphics.newImage(root .. dir ..'/'.. v) - end - gme.psel = math.min(2, #gme.pics) - end - if not fi then - gme.name = dir - end - table.insert(gms, gme) -end - -for _, v in pairs(love.filesystem.getDirectoryItems(root)) do - local file = v..'/'.. 'info.ll' - gmeNew(love.filesystem.read(root .. file), v, file) -end - -local pf, mx, mb, mpb - -local pcht = 60 * 5 -local pchat = math.floor(1 / pcht * 2 * 255 + 0.5) -local pchv, pcha = 0, 0 -local function llUpdate() - mx = love.mouse.getX() - mpb = mb - mb = 0 - if love.mouse.isDown(1) then mb = 1 - end - - if mpb == 1 and mb == 0 then - if mx <= cx then - sel = sel - 1 - if sel < 1 then sel = #gms end - elseif mx >= cx + cw then - sel = sel + 1 - if sel > #gms then sel = 1 end - else sgme = gms[sel] - end - end - - pchv = pchv + 1 - if pchv >= pcht then - pchv, pcha = 0, 0 - for _, v in pairs(gms) do - if v.pics then - v.ppsl = v.psel - v.psel = v.psel + 1 - if v.psel > #v.pics - then v.psel = 1 end - end - end - else pcha = math.min(255, pcha + pchat) - end -end - -local function llDraw() - love.graphics.polygon('fill', - 8, H / 2, - 32, H / 2 - 32, - 32, H / 2 + 32) - - love.graphics.polygon('fill', - W - 8, H / 2, - W - 32, H / 2 - 32, - W - 32, H / 2 + 32) - - local oy, t = 0, '' - local gme = gms[sel] - if gme then - local ph = ch / 1.5 - if gme.pics then - local p, n = gme.pics[gme.ppsl], gme.pics[gme.psel] - love.graphics.draw(p, cx, cy, 0, cw / p:getWidth(), ph / p:getHeight()) - love.graphics.setColor(255, 255, 255, pcha / COLDIV) - love.graphics.draw(n, cx, cy, 0, cw / n:getWidth(), ph / n:getHeight()) - love.graphics.setColor(255, 255, 255, 255) - end - love.graphics.rectangle('line', cx, cy, cw, ph) - love.graphics.rectangle('line', cx, cy, cw, ch) - - oy = cy + ph - love.graphics.setFont(bf) - love.graphics.print(gme.name, cx, oy) - oy = oy + bf:getHeight(gme.name) - love.graphics.setFont(f) - love.graphics.print(gme.desc, cx, oy) - - else oy = H / 2 - t = 'No games' - love.graphics.setFont(bf) - love.graphics.print(t, (W - bf:getWidth(t)) / 2, oy) - oy = oy + bf:getHeight(t) - love.graphics.setFont(f) - t = 'There is no projects/games to run' - love.graphics.print(t, (W - f:getWidth(t)) / 2, oy) - end -end - -function llHome() - escChroot() - love.event.push('quit', 'restart') +if love.errorhandler +then love.errorhandler = error +else love.errhand = error end local brk = false -while not brk and not sgme do +while not brk and not ll.mdir do -- event handling love.event.pump() - for n, a,b,c,d,e,f in love.event.poll() do - if n == 'quit' then - escChroot() + for n, a,b,c in love.event.poll() do + if n == 'quit' + or (n == 'keypressed' and a == 'escape') + then ll.umount() love.event.push('quit') brk = true; break end - love.handlers[n](a,b,c,d,e,f) + love.handlers[n](a,b,c) end -- update and drawing - llUpdate() + ll.skin.update() love.graphics.origin() love.graphics.clear(0, 0, 0) - llDraw() + ll.skin.draw() love.graphics.present() love.timer.sleep(0.001) end -if sgme then +if ll.mdir then love.graphics.setNewFont() resize = nil - chroot(root .. sgme.dir, sgme.main) + + love.graphics.clear(0, 0, 0) + splash() + love.graphics.present() + + if ll.skin.lovecb then + for _, v in pairs(ll.skin.lovecb) + do love[v] = nil + end + end + + love.filesystem.setIdentity(ll.mgme.dir) + local f, err = load(love.filesystem.read(ll.mgme.main), ll.mgme.name) + if not f then error(err) + else xpcall(f, function(e) + error(e) + llHome() + end) + end + + love.resize(love.graphics.getDimensions()) end diff --git a/readme.md b/readme.md index 46bbbf1..f0871b3 100644 --- a/readme.md +++ b/readme.md @@ -2,31 +2,99 @@ Custom menu for selecting game from multiple for Love2D. +![PSP Style](./scr/psp.png) + +[Other screenshots](./scr) + # How it works? -If simple: just place games in `games` folder with structure like `games/yourgame/main.lua` +Just place games into `games` folder! (like `this_folder/games/game/main.lua`) -If technically: creates local variables functions and variables. -Creates the loop until game wasn't selected with manual event handling and redrawing. +Technically, this creates the loop until game wasn't selected or user wants to exit +with custom event handling and redrawing. -# Things left or API +# LibLL -We are not keep environment super clear (except local variables ;)) -so there are some variables can be used in game: +Love Loader from 2.0 includes backend API to simplify creating custom skins. -`W` and `H` variables: width and height of the screen, controlled by love.resize function. +It have not so many functions and fields: -`love.resize` and optional `resize` payload: functions called when screen size when changed and at boot. +- `ll.games` - field for games, which have this structure: + ```lua + { + name = 'string', -- Friendly name for game or placeholder + desc = 'string', -- Description for game or placeholder + base = 'string', -- base directory used in game mounting, must end with `/` + dir = 'string', -- directory name, used if no name was defined + main = 'string', -- main file to execute or `main.lua` + screens = {'array of', 'path to screenshots'}, + scrcur = 1, --[[number]] -- current index from screenshots + scrprv = 1, --[[number]] -- previous index from screenshots + dat = nil, --[[any]] -- maybe platform-dependent data to reduce operations + } + ``` -`love.event.quit`: function to quit to menu screen +- `ll.mdir` - string or nil, contains full mounted directory. -`COLDIV`: color divider (1 or 255) to setColor function +- `ll.mgme` - game or nil, contains mounted game. -`llUsed`: is Love Loader used +- `ll.gameNew(configuration --[[string]], fileName --[[string, not used]], base --[[string]], directory --[[string]])` -`llHome`: function to quit to menu screen + Creates game object (defined above) and returns it. -They also can be used without Love Loader if load `ll-min.lua` +- `ll.gameAdd(conf, file, base, dir)` - same as `ll.gameNew` with insertion into `ll.games`. + +- `ll.addGame(fileName, fileContent)` - function for file dropping, reserved for v3.0. + +- `ll.mount(game)` - mounts game. + + Can throw an error. + + Sets `ll.mdir` and `ll.mgme`. + +- `ll.umount()` - unmounts game if can. + + Unsets `ll.mdir` and `ll.mgme`. + +- `ll.home()` - calls `llHome` + +- `ll.dt` - is developer tools enabled? + +- `__LL` - global variable of Love Loader instance when developer tools enabled. + +- `ll.devtools()` - enable developer tools. + +- `ll.fsIsAbs(file)` - is file absolute (/file)? + +- `ll.fsIsRel(file)` - is file relative, inverted result of ll.fsIsAbs (./file). + +- `ll.fsDir(path)` - get directory name (2 from /1/2/3.file). + +- `ll.fsFile(path)` - get file (including dividers after) (2 from /1/2/). + +- `ll.kbInit(direction --[[string: *, h, v, x, y]], c1 --[[number, coordinate before card for mouse (left/top)]], c2 --[[number, coordinate after card for mouse (right/bottom)]], clim --[[other coordinate limit for mouse or -1 to disable]])` - initialize keyboard module for skins. + +- `ll.kbGet() --[[nil, string: <, >, o, m, ^ anv v if direction is *]]` - get key pressed. + +# API + +To reduce things to do for game developers, this loader creates some global variables to use. + +You can also use it without Love Loader (or if your game can distribute without loader) by including `ll-min.lua` file. + +`W` and `H`: width and height of the screen which controls by custom love.resize function. + +`love.resize` and optional `resize` payload: functions called when screen size was changed and on booting. + +`love.event.quit`: function to exit to main screen. + +`llHome`: function called by `love.event.quit` (broken for now). + +`llUsed`: is Love Loader (not minimal API) used. + +`COLDIV`: color divider (1 or 255) to `love.graphics.setColor` function. + +`MOBILE`: is this device runs Android or iOS? # Fill game information @@ -40,5 +108,6 @@ name = New awesome game using Love Loader desc = Some descripion about the game. # main = optional main file instead of `main.lua` pic = screen.png -pics = [ screen.png; screen2.png ] # wow array +pics = [ screen.png; screen2.png ] # wow arrays ``` + diff --git a/scr/base.png b/scr/base.png new file mode 100644 index 0000000..4761cbb Binary files /dev/null and b/scr/base.png differ diff --git a/scr/gui.png b/scr/gui.png new file mode 100644 index 0000000..6afbc21 Binary files /dev/null and b/scr/gui.png differ diff --git a/scr/psp.png b/scr/psp.png new file mode 100644 index 0000000..be2d517 Binary files /dev/null and b/scr/psp.png differ diff --git a/skins/base.lua b/skins/base.lua new file mode 100644 index 0000000..ca31268 --- /dev/null +++ b/skins/base.lua @@ -0,0 +1,190 @@ +return function(ll) + +-- Basic cmd-like style UI +-- FIXME: Console scrolling + +local cw, ch, ctw, cth +local ct = 'Love Loader v2.0\n(c) Er2 2022\tZlib License\n\n' +local cce = false +local buf = '' + +local mb, mpb, mt = 0, 0, 0 + +local function ccp() + local argv = {} + for v in buf:gmatch '%S+' + do table.insert(argv, v) + end + local cmd = (argv[1] or ''):lower() + if cmd == '' then return '' -- nothing + elseif cmd == 'help' then return 'Available commands:' + .. '\nhelp\t\tThis command' + .. '\nls\t\tList of games' + .. '\ndir\t\tSame as ls' + .. '\ncat\t\tInformation about game' + .. '\ninfo\t\tSame as cat' + .. '\nrun\t\tStart game' + .. '\nmount\t\tSame as run' + .. '\nchroot\t\tSame as run' + .. '\nclear\t\tClear screen' + .. '\neval\t\tExecute code' + elseif cmd == 'clear' then + ct = '' + return '' + elseif cmd == 'eval' then + local code = buf:match '^%S+%s+(.*)' + local f, err = load(code, 'test.lua') + if not f then return err + else return tostring(f() or 'nil') + end + elseif cmd == 'ls' + or cmd == 'dir' then + local r = '' + if #ll.games == 0 then + return ('%s: No games found. Add some to the library.'):format(argv[1]) + end + for i = 1, #ll.games do + local v = ll.games[i] + r = r .. ('%s\t(%s)\t - %s\n'):format(v.dir, v.name, v.desc) + end + return r .. ('\nTotal: %3d games\n'):format(#ll.games) + elseif cmd == 'cat' + or cmd == 'info' then + if not argv[2] + then return ('%s: Game directory missing'):format(argv[1]) + end + local r = '' + for _, v in pairs(ll.games) do + if argv[2] == v.dir + or argv[2] == v.base .. v.dir + then r = r.. + ( '\nFriendly name:\t%s' + ..'\nDescription:\t%s' + ..'\nLocation:\t%s%s' + ..'\nMain file:\t%s' + ..'\n\n\t\tScreenshots are not supported yet.' + ..'\n'):format(v.name, v.desc, v.base, v.dir, v.main) + end + end + if r == '' then r = 'Not found' end + return r + elseif cmd == 'run' + or cmd == 'mount' + or cmd == 'chroot' then + local gme + for _, v in pairs(ll.games) do + if argv[2] == v.dir + or argv[2] == v.base .. v.dir + then if gme then + return 'Error: multiple paths found, use full location' + end + gme = v end + end + if gme + then ll.mount(gme) + love.keyboard.setTextInput(false) + return '' + else return 'Not found' + end + else return 'Unknown command "'.. cmd .. '"' + end +end + +local function update() + mpb = mb + if love.mouse.isDown(1) then mb = 1 + if mpb ~= 1 + then mt = os.time() + end + else mb = 0 + end + + if mb == 0 and mpb == 1 + and os.time() - mt <= 1 then + if love.system.getOS() == 'Android' then + love.keyboard.setTextInput( + not love.keyboard.hasTextInput(), + 0, H, W, ch) + end + end + + if not cce then + cce = true + local r = ccp() + if #r ~= 0 then ct = ct.. '\n'.. r end + ct = ct.. '\n> ' + buf = '' + end +end + +local utf8 = require 'utf8' +local function draw() + love.graphics.setColor(255, 255, 255) + + local x, y = 0, 0 + for p, cde in utf8.codes(ct) do + local chr = utf8.char(cde) + if chr == '\n' + then x, y = 0, y + 1 + elseif chr == '\r' + then x = 0 + elseif chr == '\t' + then x = x + 8 - (x % 8) + elseif chr == '\v' + then y = y + 1 + else love.graphics.print(chr, x * cw, y * ch) + if x >= ctw + then x, y = x - ctw, y + 1 + else x = x + 1 + end + end + end + if os.time() % 2 == 0 + then love.graphics.rectangle('fill', x * cw, y * ch, cw, ch) + end +end + +function resize() + local f = love.graphics.newFont(math.min(W, H) / 32) + love.graphics.setFont(f) + cw, ch = f:getWidth 'm', f:getHeight 'A' + ctw, cth = + math.floor(W / cw) - 1, + math.floor(H / ch) - 1 + + love.keyboard.setTextInput(true, 0, H, W, ch) +end + +function love.textinput(txt) + ct = ct.. txt + buf = buf.. txt +end + +function love.keypressed(k) + if k == 'backspace' then + local off = utf8.offset(buf, -1) + if off then + buf = buf:sub(1, off - 1) + off = utf8.offset(ct, -1) + if off + then ct = ct:sub(1, off - 1) + end + end + elseif k == 'return' + then cce = false + end +end + +love.keyboard.setKeyRepeat(true) +love.window.setMode(800, 600, {resizable = true}) + +return { + update = update, + draw = draw, + lovecb = { + 'textinput', + 'keypressed', + }, +} + +end diff --git a/skins/gui.lua b/skins/gui.lua new file mode 100644 index 0000000..fdb2708 --- /dev/null +++ b/skins/gui.lua @@ -0,0 +1,198 @@ +return function(ll) + +ll.cfg.pcht = ll.cfg.pcht or 60 * 5 + +local pikchv, pikcha = 0, 0 +local pikchao = math.floor(1 / ll.cfg.pcht * 2 * 255 + 0.5) + +local cx, cy, cw, ch +local f, bf + +local sel = 1 +local cdir + +function resize() + cw = W / 1.25 + ch = H / 1.25 + cx = (W - cw) / 2 + cy = (H - ch) / 2 + + local th = math.min(W, H) / 8 + + f = love.graphics.newFont(th / 3) + bf = love.graphics.newFont(th / 2) + + ll.kbInit('h', cx, cx + cw) +end + +local function update() + local sdi = ll.kbGet() + + if cdir ~= sdi then + cdir = sdi + if sdi == '<' + then sel = sel - 1 + elseif sdi == '>' + then sel = sel + 1 + elseif sdi == 'o' and ll.games[sel] + then ll.mount(ll.games[sel]) + + elseif sdi == 'm' + and love.mouse.getX() >= W - 8 + then ll.devtools() + end + + if sel < 1 then sel = #ll.games end + if sel > #ll.games then sel = 1 end + end + + pikchv = pikchv + 1 + if pikchv >= ll.cfg.pcht then + pikchv = 0 + pikcha = 0 + for _, v in pairs(ll.games) do + if v.dat.scr then + v.scrprv = v.scrcur + v.scrcur = v.scrcur + 1 + if v.scrcur > #v.dat.scr + then v.scrcur = 1 + end + end + end + else pikcha = math.min(255, pikcha + pikchao) + end +end + +local tm = 0 +local function draw() + love.graphics.setColor(0 / COLDIV, 50 / COLDIV, 75 / COLDIV) + love.graphics.rectangle('fill', 0, 0, W, H) + love.graphics.setColor(255, 255, 255, 100 / COLDIV) + love.graphics.setLineWidth(8) + love.graphics.setFont(bf) + local t = 'Love Loader' + local tw, th = bf:getWidth(t), bf:getHeight(t) + love.graphics.print(t, W - tw - 8, H - th) + + tm = (tm + 0.02) % 6.28 + local c, pc, ps = tm, math.cos(tm), math.sin(tm) + local oy = H / 2 + for x = 0, W + 8, 8 do + c = c + 0.1 + local c, s = math.cos(c), math.sin(c) + local cy, ncy, sy, nsy = + pc * H / 12, + c * H / 12, + ps * H / 12, + s * H / 12 + + love.graphics.line(x - 8, sy + oy, x-1, nsy + oy) + love.graphics.line(x - 8, sy/2 + oy, x-1, nsy/2 + oy) + love.graphics.line(x - 8, cy/1.5 + oy, x-1, ncy/1.5 + oy) + love.graphics.line(x - 8, cy*1.5 + oy, x-1, ncy*1.5 + oy) + pc, ps = c, s + end + love.graphics.setColor(255, 255, 255, 255) + love.graphics.setLineWidth(1) + + local oy, t = 0, '' + local gme = ll.games[sel] + if gme then + love.graphics.polygon('fill', + 8, H / 2, + 32, H / 2 - 32, + 32, H / 2 + 32) + + love.graphics.polygon('fill', + W - 8, H / 2, + W - 32, H / 2 - 32, + W - 32, H / 2 + 32) + + love.graphics.stencil(function() + love.graphics.rectangle('fill', cx, cy, cw, ch, 16) + end) + love.graphics.setStencilTest('greater', 0) + love.graphics.setColor(50 / COLDIV, 50 / COLDIV, 50 / COLDIV, 100 / COLDIV) + love.graphics.rectangle('fill', cx, cy, cw, ch) + love.graphics.setColor(255, 255, 255, 255) + + if gme.dat.scr then + local p, n = gme.dat.scr[gme.scrprv], gme.dat.scr[gme.scrcur] + love.graphics.draw(p, cx, cy, 0, cw / p:getWidth(), ch / p:getHeight()) + love.graphics.setColor(255, 255, 255, pikcha / COLDIV) + love.graphics.draw(n, cx, cy, 0, cw / n:getWidth(), ch / n:getHeight()) + love.graphics.setColor(0, 0, 0, 150 / COLDIV) + love.graphics.rectangle('fill', cx, cy, cw, ch) + love.graphics.setColor(255, 255, 255, 255) + end + + oy = cy + ch / 1.25 + love.graphics.setFont(bf) + love.graphics.print(gme.name, cx + 16, oy) + oy = oy + bf:getHeight(gme.name) + love.graphics.setFont(f) + love.graphics.print(gme.desc, cx + 16, oy) + love.graphics.setStencilTest() + love.graphics.rectangle('line', cx, cy, cw, ch, 16) + + elseif ll.games[1] then + sel = 1 + love.graphics.setColor(255, 255, 255, 255) + love.graphics.print('Sorry :)', W / 2, H / 2) + else love.graphics.setColor(255, 255, 255, 255) + oy = H / 2.5 + t = 'No games' + love.graphics.setFont(bf) + love.graphics.print(t, (W - bf:getWidth(t)) / 2, oy) + oy = oy + bf:getHeight(t) + love.graphics.setFont(f) + t = 'There are no projects/games to run' + love.graphics.print(t, (W - f:getWidth(t)) / 2, oy) + end +end + +function error(msg) + msg = tostring(msg) + + love.graphics.reset() + love.graphics.origin() + local bf = love.graphics.newFont(64) + local f = love.graphics.setNewFont(16) + local perc = 0 + + local q + repeat + perc = math.min(100, perc + 0.1 * math.random(50)) + + love.graphics.clear(17/COLDIV, 113/COLDIV, 172/COLDIV) + love.graphics.setColor(255, 255, 255) + love.graphics.setFont(bf) + love.graphics.print(':(', 64, 16) + love.graphics.setFont(f) + love.graphics.print(('%d%% complete'):format(perc), 64, 100) + love.graphics.printf(msg, 64, 132, W - 64) + love.graphics.print('(c) Love Loader and Er2', 8, H - 16 - 8) + love.graphics.present() + + love.event.pump() + for n, a,b,c in love.event.poll() do + if n == 'quit' + or n == 'mousereleased' + or (n == 'keypressed' and a == 'escape') + then q = true end + end + + love.timer.sleep(0.5) + until q or perc == 100 + + -- Unfortunately, we can't restart Love in error handler + llHome() -- but can outside of love.errorhandler + return function() return 1 end +end + +return { + update = update, + draw = draw, +} + +end diff --git a/skins/psp.lua b/skins/psp.lua new file mode 100644 index 0000000..2427148 --- /dev/null +++ b/skins/psp.lua @@ -0,0 +1,159 @@ +return function(ll) + +-- New UI from Sony PSP + +local sel = 1 +local psel + +local cx, cy, cw, ch, cg +local css, rcss = 2, 0 + +local f, bf + +ll.cfg.pcht = ll.cfg.pcht or 60 * 5 + +local pikchv, pikcha = 0, 0 +local pikchao = math.floor(1 / ll.cfg.pcht * 2 * 255 + 0.5) + +local logger = '' +local chc = '<>><' + +function resize() + r = math.min(W, H) / 30 + + cg = H / 64 + cw = W / 5 + ch = (H - cg) / 6 + cx = W / 10 + cy = -H / 2 + + f = love.graphics.setNewFont(math.min(W, H) / 30) + bf = love.graphics.newFont(math.min(W, H) / 20) + + ll.kbInit('v', H / 2 - ch, H / 2 + ch + cg, cx + cw * 2) +end + +love.window.setMode(800, 600, {resizable = true}) + +local sdir +local function update() + local ty = H / 2 - (ch + cg) * sel + cy = cy + (ty - cy) * 0.1 + css = css + (2 - css) * 0.2 + rcss = 2 - css + 1 + + local sdi = ll.kbGet() + + if sdi ~= sdir then + if sdi + then rcss = css + css = 1 + psel = sel + logger = logger .. sdi + if logger == chc then + resize() + ll.devtools() + end + end + sdir = sdi + if sdi == '<' + then sel = sel - 1 + elseif sdi == '>' + then sel = sel + 1 + elseif sdi == 'o' and ll.games[sel] + then ll.mount(ll.games[sel]) + end + + if sel < 1 then sel = #ll.games end + if sel > #ll.games then sel = 1 end + end + + pikchv = pikchv + 1 + if pikchv >= ll.cfg.pcht then + pikchv = 0 + pikcha = 0 + for _, v in pairs(ll.games) do + if v.dat.scr then + v.scrprv = v.scrcur + v.scrcur = v.scrcur + 1 + if v.scrcur > #v.dat.scr + then v.scrcur = 1 + end + end + end + else pikcha = math.min(255, pikcha + pikchao) + end +end + +local function scrCard(gme, x, y, w, h, a) + if not gme.dat.scr then return end + love.graphics.setColor(255, 255, 255, a / COLDIV) + local p, n = gme.dat.scr[gme.scrprv], gme.dat.scr[gme.scrcur] + love.graphics.draw(p, x, y, 0, w / p:getWidth(), h / p:getHeight()) + love.graphics.setColor(255, 255, 255, math.min(a, pikcha) / COLDIV) + love.graphics.draw(n, x, y, 0, w / n:getWidth(), h / n:getHeight()) + love.graphics.setColor(0, 0, 0, 150 / COLDIV) + love.graphics.rectangle('fill', x, y, w, h) +end + +local function draw() + love.graphics.setColor(0 / COLDIV, 50 / COLDIV, 75 / COLDIV) + love.graphics.rectangle('fill', 0, 0, W, H) + if ll.games[sel] then + local cur = ll.games[sel] + scrCard(cur, 0, 0, W, H, (css - 1) * 255) + love.graphics.setColor(255, 255, 255, 255) + love.graphics.setFont(bf) + love.graphics.printf(cur.desc, cx + cw * 2, cx, W - (cx + cw * 2)) + else love.graphics.setColor(255, 255, 255, 255) + sel = #ll.games + end + love.graphics.setFont(f) + local oy = 0 + for k, v in pairs(ll.games) do + local x, w, h, g = cx, cw, ch, cg + if k == sel then + x = cx / css + w = cw * css + h = ch * css + g = cg * 3 + oy = oy + cg * css + elseif k == psel then + x = cx / rcss + w = cw * rcss + h = ch * rcss + oy = oy + cg * rcss + end + + love.graphics.stencil(function() + love.graphics.rectangle('fill', x, cy + oy, w, h, r) + end) + love.graphics.setStencilTest('greater', 0) + + love.graphics.setColor(50 / COLDIV, 50 / COLDIV, 50 / COLDIV, 100 / COLDIV) + love.graphics.rectangle('fill', x, cy + oy, w, h) + love.graphics.setColor(255, 255, 255, 255) + + scrCard(v, x, cy + oy, w, h, 255) + love.graphics.setColor(255, 255, 255, 255) + + local th = f:getHeight(v.name) + love.graphics.print(v.name, x + cg, cy + oy + h - th - cg) + + love.graphics.setStencilTest() + love.graphics.rectangle('line', x, cy + oy, w, h, r) + oy = oy + h + g + end + if #ll.games == 0 then + love.graphics.print('No games', cx, cy - ch) + love.graphics.setFont(bf) + love.graphics.print('Add some to the library', cx, cy) + end +end + +return { + update = update, + draw = draw, +} + +end