Skip to content

Instantly share code, notes, and snippets.

@gphg
Created October 31, 2024 12:17
Show Gist options
  • Save gphg/349d8084d3a02a35d408e7fa5368cc34 to your computer and use it in GitHub Desktop.
Save gphg/349d8084d3a02a35d408e7fa5368cc34 to your computer and use it in GitHub Desktop.
My "plug and play"-ish entry point Lua script for LOVE2D.
-------------------------------------------------------------------------------
--- main.lua v0.2
-------------------------------------------------------------------------------
local main = {
_VERSION = '0.2',
}
local arg, os, package, love, jit = arg, os, package, love, jit
local assert, pcall, require, tonumber, setmetatable, print =
assert, pcall, require, tonumber, setmetatable, print
local love_ver, getenv, filesystem, timer, report, noop, concat, maxOfMount,
RUNTIME_VERBOSE,
RUNTIME_HEADLESS,
RUNTIME_EXTENSION,
RUNTIME_RANDOMSEED,
RUNTIME_SKIP_FUSED,
RUNTIME_MOUNT_POINT,
RUNTIME_MOUNT_LIMIT,
RUNTIME_GAME_MODULE,
_ -- yes, this is a variable. DO NOT REMOVE.
maxOfMount = 99 ---@todo
function noop() end
function concat(t) return table.concat(t, ', ') end
-------------------------------------------------------------------------------
-- * lock the global table: new assigment raises error
-------------------------------------------------------------------------------
setmetatable(_G, {
__index = function(_, k) error('referenced an undefined variable: ' .. k, 2) end,
__newindex = function(_, k) error('new global variables disabled: ' .. k, 2) end,
})
-------------------------------------------------------------------------------
-- * simple console print for debug and other report
-------------------------------------------------------------------------------
do
RUNTIME_VERBOSE = tostring(os.getenv('RUNTIME_VERBOSE') or '')
report = RUNTIME_VERBOSE ~= '' and print or noop
report(jit and ('%s (%s %s)'):format(jit.version, jit.os, jit.arch) or _VERSION)
report(('arguments: %s'):format(concat(arg)))
assert(love or require 'love', 'LOVE (https://love2d.org/) is required.')
assert(love.getVersion() >= 11, 'LOVE version 11.0+ is required.')
love_ver = ('L\195\150VE %d.%d.%d (%s)'):format(love.getVersion())
filesystem = love.filesystem or require 'love.filesystem' -- future-proof
timer = love.timer or require 'love.timer'
report(love_ver)
report('love.timer.getTime at:', timer.getTime())
end
-------------------------------------------------------------------------------
-- * get environment. At the moment, cast everything locally in CAPITALIZE.
-------------------------------------------------------------------------------
do
function getenv(s)
local env = os.getenv(s)
report(s, '=', env)
return env ~= '' and env or nil
end
getenv 'LOVE_GRAPHICS_USE_OPENGLES'
getenv 'SDL_OPENGL_ES_DRIVER'
RUNTIME_HEADLESS = getenv 'RUNTIME_HEADLESS'
RUNTIME_EXTENSION = getenv 'RUNTIME_EXTENSION'
RUNTIME_RANDOMSEED = getenv 'RUNTIME_RANDOMSEED'
RUNTIME_SKIP_FUSED = getenv 'RUNTIME_SKIP_FUSED'
RUNTIME_MOUNT_POINT = getenv 'RUNTIME_MOUNT_POINT'
RUNTIME_MOUNT_LIMIT = getenv 'RUNTIME_MOUNT_LIMIT'
RUNTIME_GAME_MODULE = getenv 'RUNTIME_GAME_MODULE'
end
-------------------------------------------------------------------------------
-- * initialize math randomness
-------------------------------------------------------------------------------
do
local seed = tonumber(RUNTIME_RANDOMSEED) or os.time()
local math, love_math = math, (love.math or require 'love.math')
local step = (maxOfMount + (tonumber(('%p'):format{}) % maxOfMount))
love_math.setRandomSeed(seed)
math.randomseed(seed)
report('Set math.randomseed to:', seed, 'at', step, 'steps')
for _ = 0, step do
love_math.random()
math.random()
end
end
-------------------------------------------------------------------------------
-- * game must be fused or run on fused mode
-------------------------------------------------------------------------------
do
local root = RUNTIME_MOUNT_POINT or '/'
local limit = tonumber(RUNTIME_MOUNT_LIMIT) or 16
local skip = RUNTIME_SKIP_FUSED ~= nil
local _os, listOfItem, mount, success, failed, filePattern, registry, saveDir
local table, math, getDirectoryItems = table, math, filesystem.getDirectoryItems
function mount(x, y) return filesystem.mount(x, y, true) and x or nil end
success, failed, filePattern = {}, {}, '^[%(%)%.%%%+%-%*%?%[%]%^%$@#_&\\,:;!*"\']'
registry, saveDir = (main.mounted or {}), filesystem.getSaveDirectory()
limit = math.min(math.max(limit, 0), maxOfMount)
main.mount, main.mounted = mount, registry
-- Specific OS can't set variable environment. Assuming it is fully packaged (no fused needed)
_os = love.system.getOS()
skip = skip or _os == 'Android' or _os == 'iOS'
or filesystem.getInfo('.fuseskip') ~= nil
-- mount the source base directory (parent directory)
assert(mount(filesystem.getSourceBaseDirectory(), root) or skip,
'The game is expected to run in fused mode (with "--fused").')
-- known issue: getDirectoryItems lists items on Save Directory too!
listOfItem = getDirectoryItems(root)
table.sort(listOfItem)
for i = 1, #listOfItem do
local last, v = #registry, listOfItem[i]
if #registry > limit then break end
if #v > 4 -- 1. must be more than four characters (including the dot and extension)
and not v:match(filePattern) -- 2. must not started by special characters
and filesystem.getRealDirectory(root .. v) ~= saveDir -- 3. must not on save dir
then
local fileData, w
-- try mount ANYTHING
registry[#registry + 1] = mount(v, root)
if last >= #registry and filesystem.getInfo(v, 'file') then
-- try mount file: works anywhere, BUT contents are loaded in-memory
-- TODO: A reference to point out which path it was originally from
fileData = filesystem.newFileData(v)
registry[#registry + 1] = mount(fileData, root)
-- if not stored (as userdata), then release (free up memory)
_ = last >= #registry and fileData:release()
end
w = last >= #registry and failed or success
w[#w + 1] = v -- record path for console report
end
end
report('Successfully mounted:', concat(success))
report('Unable to mount:', concat(failed))
report('Items on root:', concat(getDirectoryItems(root)))
report('Mounting time at:', timer.getTime())
end
-------------------------------------------------------------------------------
-- * modification: package.path and package.cpath
-------------------------------------------------------------------------------
do
local sourceDir = filesystem.getSourceBaseDirectory()
local execPath = filesystem.getExecutablePath --[[ @as fun(): string ]]()
local path, cpath, sep, exec, libext
path, cpath, sep = package.path, package.cpath, package.config:sub(1, 1)
exec = execPath:gsub('\\', '/'):match('(.*/)'):gsub('/$', '')
libext = { Windows = 'dll', OSX = 'dylib', _ = 'so' }
-- Windows has "lua/?.lua" relatively to the executable
if sourceDir:lower() ~= exec:lower() then
path = ('@;!/#/?.lua;!/?/#/init.#'):gsub('.', {
['@'] = path,
['!'] = sourceDir,
['#'] = RUNTIME_EXTENSION or 'lua'
})
package.path = path:gsub('/', sep):match('^%s*(.-)%s*$')
report('Append', sourceDir, 'to package.path:', package.path)
end
-- The only module to detect OS and its arch
if jit then
cpath = ('@;!/lib/*/?.#;!/lib/?.#'):gsub('.', {
['@'] = cpath,
['!'] = sourceDir,
['*'] = (jit.os .. '_' .. jit.arch):lower(),
['#'] = libext[jit.os] or libext._
})
package.cpath = cpath:gsub('/', sep):match('^%s*(.-)%s*$')
report('Append', sourceDir, 'to package.cpath:', package.cpath)
end
end
-------------------------------------------------------------------------------
-- * exec: run the game
-------------------------------------------------------------------------------
do
local candidates, hasGame, theGame, which, window, w, h
candidates = {
--'core.nogame',
--'core.game',
'nogame',
'game',
RUNTIME_GAME_MODULE
}
-- Try the game.
for i = #candidates, 1, -1 do
which = candidates[i]
hasGame, theGame = pcall(require, which)
report(hasGame and ('Located:\t' .. which) or theGame)
if hasGame then break end
end
-- Play le ducklon nogame?
if not hasGame then
print 'No game to be found. Ctrl+C to quit.'
theGame, window = noop, (love.window or require 'love.window')
if not love.graphics.isActive() and not RUNTIME_HEADLESS then
print '\nInitiating uninitialized window and graphics.'
print 'Known issue: it crashes on a machine with no display!\n'
w, h = window.getMode()
window.setMode(w, h, {
resizable = true,
highdpi = true,
})
-- https://github.com/love2d/love/blob/11.x/src/scripts/nogame.lua
theGame = setfenv(require 'love.nogame', setmetatable({}, { __index = _G }))
end
window.setTitle(love_ver)
end
---@cast theGame fun(t: table, ...)
---Run the game.
theGame(main, ...)
end
return main
@gphg
Copy link
Author

gphg commented Oct 31, 2024

This is it.

The rest are on game.lua or game/init.lua, which must be a function or callable table. Inside it mostly booting up, love callbacks definition, and other one-time setup before the main loop.

Code on main.lua are breakdown into pieces between do and end scoop for readability. Also some variables are localized because I have a plan to minify the code exclusively main.lua and conf.lua only.

Be design, the game is expected to be played as --fused as it meant to be. And also to break up the love.filesystem limitation. There are other things that need to be explained, but here's the unfinished packaged (zipped) project build from my "private working in progress" that eventually get public.

https://files.sakamoto.moe/c1caa8b34587_universal.love

@gphg
Copy link
Author

gphg commented Oct 31, 2024

Prepacked ready to distributed (prototype) that has identical contents as above to here.

Additional information:

  • These are products from my project (hosted on GitHub privately), there aren't much, it is just embarrassing how little progress I have been doing. Contact me personally to gain the access.
  • These uploaded files are hosted on pomf-like file hosting. GitHub has restricted file type for attachment. Uploaded file stays for 90 days.
  • Executable binary (.exe) for Win32 is false positive as I checked these on VirusTotal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment