Created
October 31, 2024 12:17
-
-
Save gphg/349d8084d3a02a35d408e7fa5368cc34 to your computer and use it in GitHub Desktop.
My "plug and play"-ish entry point Lua script for LOVE2D.
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
------------------------------------------------------------------------------- | |
--- 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 |
Prepacked ready to distributed (prototype) that has identical contents as above to here.
- https://files.sakamoto.moe/5a9b70a8ee51_game-universal.zip
- https://files.sakamoto.moe/537b6977a05f_game-win32.zip
- https://files.sakamoto.moe/5563d8f90546_game-win64.zip
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
This is it.
The rest are on
game.lua
orgame/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 betweendo
andend
scoop for readability. Also some variables are localized because I have a plan to minify the code exclusivelymain.lua
andconf.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