2022-11-27 22:06:11 +00:00
-- A general architecture for free-wheeling, live programs:
-- on startup:
-- scan both the app directory and the save directory for files with numeric prefixes
2022-12-26 08:27:24 +00:00
-- from the numeric prefix in file 'head', obtain a manifest
-- load all files (which must start with a numeric prefix) from the manifest
2022-11-27 22:06:11 +00:00
--
-- then start drawing frames on screen and reacting to events
--
-- events from keyboard and mouse are handled as the app desires
--
-- on incoming messages to a specific file, however, the app must:
2022-12-26 08:27:24 +00:00
-- save the message's value to a new, smallest unused numeric prefix
2022-11-27 22:06:11 +00:00
-- execute the value
2022-12-26 08:27:24 +00:00
-- if there's an error, go back to the previous value of the same
-- definition if one exists
2022-11-27 22:06:11 +00:00
--
2022-12-26 08:27:24 +00:00
-- if a game encounters a run-time error, send it to the driver and await
-- further instructions. The app will go unresponsive in the meantime, that
-- is expected. To shut it down cleanly, type C-q in the driver.
2022-11-27 22:06:11 +00:00
-- namespace for these functions
live = { }
-- state for these functions
Live = { }
-- a namespace of frameworky callbacks
-- these will be modified live
on = { }
-- ========= on startup, load the version at head
function live . initialize ( arg )
-- version control
2022-12-25 23:51:39 +00:00
Live.head = 0
Live.next_version = 1
Live.history = { } -- array of filename roots corresponding to each numeric prefix
Live.manifest = { } -- mapping from roots to numeric prefixes as of version Live.head
2023-04-10 06:29:44 +01:00
live.freeze_all_existing_definitions ( )
2022-11-27 22:06:11 +00:00
live.load_files_so_far ( )
2022-12-25 23:51:39 +00:00
Live.previous_read = 0
2022-11-27 22:06:11 +00:00
if on.load then on.load ( ) end
end
2023-04-10 06:29:44 +01:00
-- Everything that exists before we start loading the live files is frozen and
-- can't be edited live.
function live . freeze_all_existing_definitions ( )
Live.frozen_definitions = { on = true } -- special case for version 1
local done = { }
done [ Live.frozen_definitions ] = true
live.freeze_all_existing_definitions_in ( _G , { } , done )
end
function live . freeze_all_existing_definitions_in ( tab , scopes , done )
-- track duplicates to avoid cycles like _G._G, _G._G._G, etc.
if done [ tab ] then return end
done [ tab ] = true
for name , binding in pairs ( tab ) do
local full_name = live.full_name ( scopes , name )
--? print(full_name)
Live.frozen_definitions [ full_name ] = true
if type ( binding ) == ' table ' and full_name ~= ' package ' then -- var 'package' contains copies of all modules, but not the best name; rely on people to not modify package.loaded.io.open, etc.
table.insert ( scopes , name )
live.freeze_all_existing_definitions_in ( binding , scopes , done )
table.remove ( scopes )
end
end
end
function live . full_name ( scopes , name )
local ns = table.concat ( scopes , ' . ' )
if # ns == 0 then return name end
return ns .. ' . ' .. name
end
2022-11-27 22:06:11 +00:00
function live . load_files_so_far ( )
print ( ' new edits will go to ' .. love.filesystem . getSaveDirectory ( ) )
local files = { }
live.append_files_with_numeric_prefix ( ' ' , files )
table.sort ( files )
live.check_integrity ( files )
live.append_files_with_numeric_prefix ( love.filesystem . getSaveDirectory ( ) , files )
table.sort ( files )
live.check_integrity ( files )
2022-12-25 23:51:39 +00:00
Live.history = live.load_history ( files )
Live.next_version = # Live.history + 1
2022-11-27 22:06:11 +00:00
local head_string = love.filesystem . read ( ' head ' )
2022-12-25 23:51:39 +00:00
Live.head = tonumber ( head_string )
if Live.head > 0 then
Live.manifest = json.decode ( love.filesystem . read ( live.versioned_manifest ( Live.head ) ) )
2022-11-27 22:06:11 +00:00
end
live.load_everything_in_manifest ( )
end
function live . append_files_with_numeric_prefix ( dir , files )
for _ , file in ipairs ( love.filesystem . getDirectoryItems ( dir ) ) do
2022-12-01 03:26:46 +00:00
if file : match ( ' ^%d ' ) then
table.insert ( files , file )
end
2022-11-27 22:06:11 +00:00
end
end
function live . check_integrity ( files )
local manifest_found , file_found = false , false
local expected_index = 1
for _ , file in ipairs ( files ) do
for numeric_prefix , root in file : gmatch ( ' (%d+)-(.+) ' ) do
-- only runs once
local index = tonumber ( numeric_prefix )
-- skip files without numeric prefixes
if index ~= nil then
if index < expected_index then
print ( index , expected_index )
end
assert ( index >= expected_index )
if index > expected_index then
assert ( index == expected_index + 1 )
assert ( manifest_found and file_found )
expected_index = index
manifest_found , file_found = false , false
end
assert ( index == expected_index )
2023-01-09 20:41:49 +00:00
if root == ' fwmanifest ' then
2022-11-27 22:06:11 +00:00
assert ( not manifest_found )
manifest_found = true
else
assert ( not file_found )
file_found = true
end
end
end
end
end
function live . load_history ( files )
local result = { }
for _ , file in ipairs ( files ) do
for numeric_prefix , root in file : gmatch ( ' (%d+)-(.+) ' ) do
-- only runs once
local index = tonumber ( numeric_prefix )
-- skip
if index ~= nil then
2023-01-09 20:41:49 +00:00
if root ~= ' fwmanifest ' then
2022-11-27 22:06:11 +00:00
assert ( index == # result + 1 )
table.insert ( result , root )
end
end
end
end
return result
end
function live . load_everything_in_manifest ( )
2022-11-28 02:07:10 +00:00
local files_to_load = { }
2022-12-25 23:51:39 +00:00
for k , v in pairs ( Live.manifest ) do
2023-01-03 02:21:55 +00:00
-- Most keys in the manifest are definitions. If we need to store any
-- metadata we'll do it in keys starting with a specific prefix.
if not starts_with ( k , ' fw_ ' ) then
2022-11-27 22:06:11 +00:00
local root , index = k , v
local filename = live.versioned_filename ( index , root )
2022-11-28 02:07:10 +00:00
table.insert ( files_to_load , filename )
2022-11-27 22:06:11 +00:00
end
end
2022-11-28 02:07:10 +00:00
table.sort ( files_to_load )
for _ , filename in ipairs ( files_to_load ) do
local buf = love.filesystem . read ( filename )
assert ( buf and buf ~= ' ' )
2023-01-08 04:27:38 +00:00
local status , err = live.eval ( buf )
if status == nil then
error ( ( ' error loading %s from manifest: %s ' ) : format ( filename , err ) )
end
2022-11-28 02:07:10 +00:00
end
2022-11-27 22:06:11 +00:00
end
2023-01-03 02:21:55 +00:00
PARENT = ' fw_parent '
2023-01-03 02:24:24 +00:00
APP = ' fw_app '
2023-01-03 02:21:55 +00:00
2022-11-27 22:06:11 +00:00
function live . versioned_filename ( index , root )
return ( ' %04d-%s ' ) : format ( index , root )
end
function live . versioned_manifest ( index )
2023-01-09 20:27:17 +00:00
return ( ' %04d-fwmanifest ' ) : format ( index )
2022-11-27 22:06:11 +00:00
end
-- ========= on each frame, check for messages and alter the app as needed
function live . update ( dt )
2022-12-25 23:51:39 +00:00
if Current_time - Live.previous_read > 0.1 then
2022-12-26 08:27:24 +00:00
local buf = live.receive_from_driver ( )
2022-11-27 22:06:11 +00:00
if buf then
live.run ( buf )
2022-12-24 04:30:35 +00:00
if on.code_change then on.code_change ( ) end
2022-11-27 22:06:11 +00:00
end
2022-12-25 23:51:39 +00:00
Live.previous_read = Current_time
2022-11-27 22:06:11 +00:00
end
end
-- look for a message from outside, and return nil if there's nothing
2022-12-26 08:27:24 +00:00
function live . receive_from_driver ( )
2022-12-01 03:25:34 +00:00
local f = io.open ( love.filesystem . getAppdataDirectory ( ) .. ' /_love_akkartik_driver_app ' )
2022-11-27 22:06:11 +00:00
if f == nil then return nil end
local result = f : read ( ' *a ' )
f : close ( )
if result == ' ' then return nil end -- empty file == no message
print ( ' <= ' .. color ( --[[bold]] 1 , --[[blue]] 4 ) )
print ( result )
print ( reset_terminal ( ) )
2023-02-05 05:19:05 +00:00
os.remove ( love.filesystem . getAppdataDirectory ( ) .. ' /_love_akkartik_driver_app ' )
2022-11-27 22:06:11 +00:00
return result
end
2022-12-26 08:27:24 +00:00
function live . send_to_driver ( msg )
2022-12-01 03:25:34 +00:00
local f = io.open ( love.filesystem . getAppdataDirectory ( ) .. ' /_love_akkartik_app_driver ' , ' w ' )
2022-11-27 22:06:11 +00:00
if f == nil then return end
f : write ( msg )
f : close ( )
print ( ' => ' .. color ( 0 , --[[green]] 2 ) )
print ( msg )
print ( reset_terminal ( ) )
end
2023-01-08 03:11:50 +00:00
function live . send_run_time_error_to_driver ( msg )
2022-12-01 03:25:34 +00:00
local f = io.open ( love.filesystem . getAppdataDirectory ( ) .. ' /_love_akkartik_app_driver_run_time_error ' , ' w ' )
2022-11-27 22:27:04 +00:00
if f == nil then return end
f : write ( msg )
f : close ( )
2022-11-28 02:19:58 +00:00
print ( ' => ' .. color ( 0 , --[[red]] 1 ) )
2022-11-27 22:27:04 +00:00
print ( msg )
2022-11-28 02:19:58 +00:00
print ( reset_terminal ( ) )
2022-11-27 22:27:04 +00:00
end
2022-11-27 22:06:11 +00:00
-- args:
-- format: 0 for normal, 1 for bold
-- color: 0-15
function color ( format , color )
return ( ' \027 [%d;%dm ' ) : format ( format , 30 + color )
end
function reset_terminal ( )
return ' \027 [m '
end
-- define or undefine top-level bindings
function live . run ( buf )
2022-12-26 08:27:24 +00:00
local cmd = live.get_cmd_from_buffer ( buf )
2022-11-27 22:06:11 +00:00
assert ( cmd )
print ( ' command is ' .. cmd )
if cmd == ' QUIT ' then
love.event . quit ( 1 )
2023-04-08 02:18:43 +01:00
elseif cmd == ' RESTART ' then
restart ( )
2022-11-27 22:06:11 +00:00
elseif cmd == ' MANIFEST ' then
2023-01-03 02:24:24 +00:00
Live.manifest [ APP ] = love.filesystem . getIdentity ( ) -- doesn't need to be persisted, but no harm if it does..
2022-12-26 08:27:24 +00:00
live.send_to_driver ( json.encode ( Live.manifest ) )
2022-11-27 22:06:11 +00:00
elseif cmd == ' DELETE ' then
2023-04-10 06:31:12 +01:00
local definition_name = buf : match ( ' ^%S+%s+(%S+) ' )
Live.manifest [ definition_name ] = nil
live.eval ( definition_name .. ' = nil ' ) -- ignore errors which will likely be from keywords like `function = nil`
local next_filename = live.versioned_filename ( Live.next_version , definition_name )
2022-11-27 22:06:11 +00:00
love.filesystem . write ( next_filename , ' ' )
2023-04-10 06:31:12 +01:00
table.insert ( Live.history , definition_name )
2023-04-09 18:24:23 +01:00
live.roll_forward ( )
2022-11-27 22:06:11 +00:00
elseif cmd == ' GET ' then
2023-04-10 06:31:12 +01:00
local definition_name = buf : match ( ' ^%S+%s+(%S+) ' )
local val , _ = live.get_binding ( definition_name )
2023-01-08 04:39:52 +00:00
if val then
live.send_to_driver ( val )
else
live.send_to_driver ( ' ERROR no such value ' )
end
2023-01-07 22:43:38 +00:00
elseif cmd == ' GET* ' then
-- batch version of GET
local result = { }
2023-04-10 06:31:12 +01:00
for definition_name in buf : gmatch ( ' %s+(%S+) ' ) do
print ( definition_name )
local val , _ = live.get_binding ( definition_name )
2023-01-08 04:48:22 +00:00
if val then
table.insert ( result , val )
end
2023-01-07 22:43:38 +00:00
end
2023-01-08 05:23:51 +00:00
local delimiter = ' \n ==fw: definition boundary== \n '
live.send_to_driver ( table.concat ( result , delimiter ) .. delimiter ) -- send a final delimiter to simplify the driver's task
2023-01-26 02:03:00 +00:00
elseif cmd == ' DEFAULT_MAP ' then
local contents = love.filesystem . read ( ' default_map ' )
if contents == nil then contents = ' {} ' end
live.send_to_driver ( contents )
2022-11-27 22:06:11 +00:00
-- other commands go here
else
2023-04-10 06:31:12 +01:00
local definition_name = cmd
if Live.frozen_definitions [ definition_name ] then
live.send_to_driver ( ' ERROR definition ' .. definition_name .. ' is part of Freewheeling infrastructure and cannot be safely edited live. ' )
2023-04-10 06:29:44 +01:00
return
end
2023-04-10 06:31:12 +01:00
local next_filename = live.versioned_filename ( Live.next_version , definition_name )
2022-11-27 22:06:11 +00:00
love.filesystem . write ( next_filename , buf )
2023-04-10 06:31:12 +01:00
table.insert ( Live.history , definition_name )
Live.manifest [ definition_name ] = Live.next_version
2023-04-09 18:24:23 +01:00
live.roll_forward ( )
2022-11-27 22:06:11 +00:00
local status , err = live.eval ( buf )
if not status then
2023-04-09 18:24:23 +01:00
live.roll_back ( )
2022-11-27 22:06:11 +00:00
-- throw an error
2022-12-26 08:27:24 +00:00
live.send_to_driver ( ' ERROR ' .. tostring ( err ) )
2023-01-08 04:28:37 +00:00
return
2022-11-27 22:06:11 +00:00
end
2023-01-23 07:45:44 +00:00
-- run all tests
2023-01-23 00:39:51 +00:00
Test_errors = { }
2023-01-24 04:12:23 +00:00
App.run_tests ( record_error_by_test )
2023-01-23 17:16:31 +00:00
live.send_to_driver ( json.encode ( Test_errors ) )
2023-01-23 07:45:44 +00:00
end
end
2023-04-09 18:24:23 +01:00
-- update Live.Head and record the new Live.Manifest (which caller has already modified)
function live . roll_forward ( )
Live.manifest [ PARENT ] = Live.head
local manifest_filename = live.versioned_manifest ( Live.next_version )
love.filesystem . write ( manifest_filename , json.encode ( Live.manifest ) )
Live.head = Live.next_version
love.filesystem . write ( ' head ' , tostring ( Live.head ) )
Live.next_version = Live.next_version + 1
end
-- update app.Head and reload app.Manifest appropriately
function live . roll_back ( )
Live.head = Live.manifest [ PARENT ]
love.filesystem . write ( ' head ' , tostring ( Live.head ) )
local previous_manifest_filename = live.versioned_manifest ( Live.head )
Live.manifest = json.decode ( love.filesystem . read ( previous_manifest_filename ) )
end
2022-12-26 08:27:24 +00:00
function live . get_cmd_from_buffer ( buf )
return buf : match ( ' ^%s*(%S+) ' )
end
2022-11-27 22:06:11 +00:00
function live . get_binding ( name )
2022-12-25 23:51:39 +00:00
if Live.manifest [ name ] then
return love.filesystem . read ( live.versioned_filename ( Live.manifest [ name ] , name ) )
2022-11-27 22:06:11 +00:00
end
end
-- Wrapper for Lua's weird evaluation model.
-- Lua is persnickety about expressions vs statements, so we need to do some
-- extra work to get the result of an evaluation.
-- return values:
-- all well -> true, ...
-- load failed -> nil, error message
-- run failed -> false, error message
function live . eval ( buf )
-- We assume a program is either correct with 'return' prefixed xor not.
-- Is this correct? Who knows! But the Lua REPL does this as well.
local f = load ( ' return ' .. buf , ' REPL ' )
if f then
return pcall ( f )
end
local f , err = load ( buf , ' REPL ' )
if f then
return pcall ( f )
else
return nil , err
end
end
-- ========= on error, pause the app and wait for messages
-- return nil to continue the event loop, non-nil to quit
function live . handle_error ( err )
local msg = tostring ( err )
-- draw a pause indicator on screen
love.graphics . setColor ( 1 , 0 , 0 )
love.graphics . rectangle ( ' fill ' , 10 , 10 , 3 , 10 )
love.graphics . rectangle ( ' fill ' , 16 , 10 , 3 , 10 )
love.graphics . present ( )
-- print stack trace here just in case we ran the app through a terminal
2023-01-21 04:07:34 +00:00
local stack_trace = debug.traceback ( ' Error: ' .. msg , --[[stack frame]] 2 ) : gsub ( ' \n [^ \n ]+$ ' , ' ' )
2022-11-27 22:06:11 +00:00
print ( stack_trace )
print ( ' Look in the driver for options to investigate further. ' )
print ( " (You probably can't close the app window at this point. If you don't have the driver set up, you might need to force-quit.) " )
-- send stack trace to driver and wait for a response
2022-12-26 08:27:24 +00:00
live.send_run_time_error_to_driver ( stack_trace )
2022-11-27 22:06:11 +00:00
local buf
repeat
2022-12-26 08:27:24 +00:00
buf = live.receive_from_driver ( )
2023-01-23 05:53:58 +00:00
love.timer . sleep ( 0.001 )
2022-11-27 22:06:11 +00:00
until buf
if buf == ' QUIT ' then
return true
end
live.run ( buf )
end