Initial commit
This commit is contained in:
commit
ee4762673a
14 changed files with 410 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"sumneko.lua"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"Lua.workspace.library": [
|
||||||
|
"${addons}/love2d/module/library"
|
||||||
|
],
|
||||||
|
"Lua.runtime.version": "Lua 5.2",
|
||||||
|
"Lua.runtime.special": {
|
||||||
|
"love.filesystem.load": "loadfile"
|
||||||
|
},
|
||||||
|
"Lua.workspace.checkThirdParty": false
|
||||||
|
}
|
||||||
18
README.md
Normal file
18
README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# LÖVE DOS/Native Abstraction
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Download or compile `love.exe` from [LoveDOS repo](https://github.com/rndtrash/lovedos), then put it to the `lovedos/` folder
|
||||||
|
2. Download [PowerShell 7.0+](https://github.com/PowerShell/PowerShell/releases) for Windows or Linux
|
||||||
|
3. Make a copy of a file called `example.env`, name it as just `.env`, then edit it to your preference.
|
||||||
|
On Linux, you can install LÖVE or DosBox-X with your package manager of choice and, instead of providing a complete path,
|
||||||
|
it would be enough to give only a command name
|
||||||
|
4. Launch `run.ps1` to open a LÖVE window with the game in `game/` folder, or use `run_dosbox.ps1` to open a DosBox-X window with
|
||||||
|
LoveDOS running the same game.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Visual Studio Code users may use the recommended extension [Lua by sumneko](https://marketplace.visualstudio.com/items?itemName=sumneko.lua)
|
||||||
|
with LÖVE module already enabled and Lua version set to 5.2 on workspace level.
|
||||||
|
|
||||||
|
Native version uses [lick by usysrc](https://codeberg.org/usysrc/lick) for hot reload on any change in the `game/` folder.
|
||||||
12
common.ps1
Normal file
12
common.ps1
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
Get-Content .env | ForEach-Object {
|
||||||
|
if ($_) {
|
||||||
|
$name, $value = $_.split('=', 2)
|
||||||
|
set-content env:\$name $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RunProgram([string] $program, [parameter(ValueFromRemainingArguments = $true)][string[]] $arguments) {
|
||||||
|
Start-Process -NoNewWindow -Wait -FilePath $program -ArgumentList $arguments
|
||||||
|
}
|
||||||
3
example.env
Normal file
3
example.env
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
LOVE2D_EXE=C:/Program Files/LOVE/love.exe
|
||||||
|
DOSBOX_X_EXE=C:/DOSBox-X/dosbox-x.exe
|
||||||
|
DOSBOX_X_CPU=pentium_ii
|
||||||
264
game/3rd/lick.lua
Normal file
264
game/3rd/lick.lua
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
-- lick.lua
|
||||||
|
--
|
||||||
|
-- simple LIVECODING environment for Löve
|
||||||
|
-- overwrites love.run, pressing all errors to the terminal/console or overlays it
|
||||||
|
--
|
||||||
|
|
||||||
|
local lick = {}
|
||||||
|
lick.debug = false -- show debug output
|
||||||
|
lick.reset = false -- reset the game and call love.load on file change
|
||||||
|
lick.clearFlag = false -- clear the screen on file change
|
||||||
|
lick.sleepTime = 0.001 -- sleep time in seconds
|
||||||
|
lick.showReloadMessage = true -- show message when a file is reloaded
|
||||||
|
lick.chunkLoadMessage = "CHUNK LOADED" -- message to show when a chunk is loaded
|
||||||
|
lick.updateAllFiles = false -- include files in watchlist for changes
|
||||||
|
lick.clearPackages = false -- clear all packages in package.loaded on file change
|
||||||
|
lick.defaultFile = "main.lua" -- default file to load
|
||||||
|
lick.fileExtensions = { ".lua" } -- file extensions to watch
|
||||||
|
lick.entryPoint = "main.lua" -- entry point for the game, if empty, all files are reloaded
|
||||||
|
lick.debugTextXOffset = 50 -- X offset for debug text from the center (positive moves right)
|
||||||
|
lick.debugTextWidth = 400 -- Maximum width for debug text
|
||||||
|
lick.debugTextAlpha = 0.8 -- Opacity of the debug text (0.0 to 1.0)
|
||||||
|
lick.debugTextAlignment = "right" -- Alignment of the debug text ("left", "right", "center", "justify")
|
||||||
|
|
||||||
|
-- local variables
|
||||||
|
-- No longer needed, debug_output tracks persistent errors
|
||||||
|
local last_modified = {}
|
||||||
|
local debug_output = nil
|
||||||
|
local working_files = {}
|
||||||
|
local should_clear_screen_next_frame = false -- Flag to clear screen on next draw cycle
|
||||||
|
|
||||||
|
-- Helper to handle error output and update debug_output
|
||||||
|
local function handleErrorOutput(err_message)
|
||||||
|
-- Ensure the message starts with "ERROR: " for console output if it doesn't already
|
||||||
|
local console_message = tostring(err_message)
|
||||||
|
if not console_message:find("^ERROR: ") then
|
||||||
|
console_message = "ERROR: " .. console_message
|
||||||
|
end
|
||||||
|
print(console_message)
|
||||||
|
|
||||||
|
-- Update debug_output for on-screen display
|
||||||
|
if debug_output then
|
||||||
|
debug_output = debug_output .. console_message .. "\n"
|
||||||
|
else
|
||||||
|
debug_output = console_message .. "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Error handler wrapping for pcall
|
||||||
|
local function handle(err)
|
||||||
|
return "ERROR: " .. err
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Function to collect all files in the directory and subdirectories with the given extensions into a set
|
||||||
|
local function collectWorkingFiles(file_set, dir)
|
||||||
|
dir = dir or ""
|
||||||
|
local files = love.filesystem.getDirectoryItems(dir)
|
||||||
|
for _, file in ipairs(files) do
|
||||||
|
local filePath = dir .. (dir ~= "" and "/" or "") .. file
|
||||||
|
local info = love.filesystem.getInfo(filePath)
|
||||||
|
if info and info.type == "file" then
|
||||||
|
for _, ext in ipairs(lick.fileExtensions) do
|
||||||
|
if file:sub(- #ext) == ext then
|
||||||
|
file_set[filePath] = true -- Add to set for uniqueness
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif info and info.type == "directory" then
|
||||||
|
collectWorkingFiles(file_set, filePath)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialization
|
||||||
|
local function load()
|
||||||
|
-- Clear previous working files to prevent accumulation if load() is called multiple times
|
||||||
|
working_files = {}
|
||||||
|
|
||||||
|
if not lick.updateAllFiles then
|
||||||
|
table.insert(working_files, lick.defaultFile)
|
||||||
|
else
|
||||||
|
local file_set = {}
|
||||||
|
collectWorkingFiles(file_set, "") -- Start collection from root directory
|
||||||
|
-- Convert set to ordered list
|
||||||
|
for file_path, _ in pairs(file_set) do
|
||||||
|
table.insert(working_files, file_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize the last_modified table for all working files
|
||||||
|
for _, file in ipairs(working_files) do
|
||||||
|
local info = love.filesystem.getInfo(file)
|
||||||
|
-- Ensure info exists before accessing modtime; set to 0 or current time if file not found
|
||||||
|
if info then
|
||||||
|
last_modified[file] = info.modtime
|
||||||
|
else
|
||||||
|
-- If a file listed in working_files doesn't exist, treat its modtime as 0
|
||||||
|
-- This ensures it will appear as "modified" if it ever appears later.
|
||||||
|
last_modified[file] = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reloadFile(file)
|
||||||
|
local success, chunk = pcall(love.filesystem.load, file)
|
||||||
|
if not success then
|
||||||
|
handleErrorOutput(chunk)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if chunk then
|
||||||
|
local ok, err = xpcall(chunk, handle)
|
||||||
|
if not ok then
|
||||||
|
handleErrorOutput(err)
|
||||||
|
else
|
||||||
|
if lick.showReloadMessage then print(lick.chunkLoadMessage) end
|
||||||
|
debug_output = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if lick.reset and love.load then
|
||||||
|
local loadok, err = xpcall(love.load, handle)
|
||||||
|
if not loadok then -- Always report load errors
|
||||||
|
handleErrorOutput(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if a file is modified, reload relevant files
|
||||||
|
local function checkFileUpdate()
|
||||||
|
local any_file_modified = false
|
||||||
|
local files_actually_modified = {} -- Store paths of files whose modtime has changed
|
||||||
|
|
||||||
|
for _, file_path in ipairs(working_files) do
|
||||||
|
local info = love.filesystem.getInfo(file_path)
|
||||||
|
-- Check if file exists and its modification time has changed
|
||||||
|
-- Use `or 0` for `last_modified[file_path]` to handle cases where it might not be initialized,
|
||||||
|
-- ensuring `info.modtime` (if exists) is always greater than 0.
|
||||||
|
if info and info.type == "file" and info.modtime and info.modtime > (last_modified[file_path] or 0) then
|
||||||
|
any_file_modified = true
|
||||||
|
table.insert(files_actually_modified, file_path)
|
||||||
|
last_modified[file_path] = info.modtime -- Update the last modified time
|
||||||
|
elseif not info and last_modified[file_path] ~= nil then
|
||||||
|
-- Handle case where a previously tracked file no longer exists (it was deleted)
|
||||||
|
-- This means its state has changed.
|
||||||
|
any_file_modified = true
|
||||||
|
last_modified[file_path] = 0 -- Set to 0 so if it reappears, it's detected as modified
|
||||||
|
-- Note: We don't add deleted files to `files_actually_modified` because `reloadFile`
|
||||||
|
-- would fail if called on a non-existent file. The effect of deletion is usually
|
||||||
|
-- handled by re-running the entry point or by the user.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not any_file_modified then
|
||||||
|
return -- No files changed, nothing to do
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If lick.clearFlag is true, set a flag to clear the screen on the next draw
|
||||||
|
if lick.clearFlag then
|
||||||
|
should_clear_screen_next_frame = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If any file was modified, clear packages from the require cache if configured
|
||||||
|
if lick.clearPackages then
|
||||||
|
for k, _ in pairs(package.loaded) do
|
||||||
|
package.loaded[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if lick.entryPoint ~= "" then
|
||||||
|
-- If an entry point is defined, reload it. This ensures the entire game logic
|
||||||
|
-- (which might implicitly depend on modified files) is re-executed.
|
||||||
|
reloadFile(lick.entryPoint)
|
||||||
|
else
|
||||||
|
-- If no specific entry point, only reload the files that were actually modified.
|
||||||
|
for _, file_path in ipairs(files_actually_modified) do
|
||||||
|
reloadFile(file_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- last_modified for files that actually changed was updated in the initial loop.
|
||||||
|
-- For files that didn't change, their last_modified values remain correct.
|
||||||
|
-- If a file was deleted, its last_modified is set to 0.
|
||||||
|
-- No further global update loop for last_modified is needed.
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update(dt)
|
||||||
|
checkFileUpdate()
|
||||||
|
if not love.update then return end
|
||||||
|
local updateok, err = pcall(love.update, dt)
|
||||||
|
if not updateok then -- Always report update errors
|
||||||
|
handleErrorOutput(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw()
|
||||||
|
local drawok, err = xpcall(love.draw, handle)
|
||||||
|
if not drawok then -- Always report draw errors
|
||||||
|
handleErrorOutput(err)
|
||||||
|
end
|
||||||
|
|
||||||
|
if lick.debug and debug_output then
|
||||||
|
love.graphics.setColor(1, 1, 1, lick.debugTextAlpha)
|
||||||
|
love.graphics.printf(debug_output, (love.graphics.getWidth() / 2) + lick.debugTextXOffset, 0, lick
|
||||||
|
.debugTextWidth, lick.debugTextAlignment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function love.run()
|
||||||
|
load()
|
||||||
|
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
|
||||||
|
|
||||||
|
-- Workaround for macOS random number generator issue
|
||||||
|
-- On macOS, the random number generator can produce the same sequence of numbers
|
||||||
|
-- if not properly seeded. This workaround ensures that the random number generator
|
||||||
|
-- is seeded correctly to avoid this issue.
|
||||||
|
if jit and jit.os == "OSX" then
|
||||||
|
math.randomseed(os.time())
|
||||||
|
math.random()
|
||||||
|
math.random()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We don't want the first frame's dt to include time taken by love.load.
|
||||||
|
if love.timer then love.timer.step() end
|
||||||
|
|
||||||
|
local dt = 0
|
||||||
|
|
||||||
|
return function()
|
||||||
|
if love.event then
|
||||||
|
love.event.pump()
|
||||||
|
for name, a, b, c, d, e, f in love.event.poll() do
|
||||||
|
if name == "quit" then
|
||||||
|
if not love.quit or not love.quit() then
|
||||||
|
return a or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
love.handlers[name](a, b, c, d, e, f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update dt, as we'll be passing it to update
|
||||||
|
if love.timer then
|
||||||
|
dt = love.timer.step()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Call update and draw
|
||||||
|
if update then update(dt) end -- will pass 0 if love.timer is disabled
|
||||||
|
if love.graphics and love.graphics.isActive() then
|
||||||
|
love.graphics.origin()
|
||||||
|
-- Clear the screen based on lick.clearFlag and file modification
|
||||||
|
if lick.clearFlag and should_clear_screen_next_frame then
|
||||||
|
love.graphics.clear(love.graphics.getBackgroundColor())
|
||||||
|
should_clear_screen_next_frame = false -- Reset the flag after clearing
|
||||||
|
elseif not lick.clearFlag then
|
||||||
|
-- If lick.clearFlag is false, clear the screen every frame (default behavior)
|
||||||
|
love.graphics.clear(love.graphics.getBackgroundColor())
|
||||||
|
end
|
||||||
|
if draw then draw() end
|
||||||
|
love.graphics.present()
|
||||||
|
end
|
||||||
|
|
||||||
|
if love.timer then love.timer.sleep(lick.sleepTime) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return lick
|
||||||
30
game/main.lua
Normal file
30
game/main.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
if love.system.getOS() ~= 'DOS' then
|
||||||
|
local lick = require "3rd.lick"
|
||||||
|
lick.updateAllFiles = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local platform = require "platform.platform"
|
||||||
|
|
||||||
|
function love.load()
|
||||||
|
platform.init()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawText(str, x, y)
|
||||||
|
local font = love.graphics.getFont()
|
||||||
|
love.graphics.print(str, x - font:getWidth(str) / 2, y - font:getHeight() / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.draw()
|
||||||
|
platform.drawStart()
|
||||||
|
|
||||||
|
love.graphics.clear()
|
||||||
|
drawText('Hellorld!', 160, 100)
|
||||||
|
|
||||||
|
platform.drawEnd()
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.keypressed(key)
|
||||||
|
if key == "escape" then
|
||||||
|
os.exit()
|
||||||
|
end
|
||||||
|
end
|
||||||
15
game/platform/dos.lua
Normal file
15
game/platform/dos.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
local platform = {}
|
||||||
|
|
||||||
|
function platform.init()
|
||||||
|
-- TODO: NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
function platform.drawStart()
|
||||||
|
-- TODO: NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
function platform.drawEnd()
|
||||||
|
-- TODO: NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
return platform
|
||||||
24
game/platform/native.lua
Normal file
24
game/platform/native.lua
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
local platform = {}
|
||||||
|
|
||||||
|
local screenWidth = 320
|
||||||
|
local screenHeight = 200
|
||||||
|
local screenScale = 2
|
||||||
|
local canvas
|
||||||
|
|
||||||
|
function platform.init()
|
||||||
|
love.graphics.setDefaultFilter("nearest")
|
||||||
|
|
||||||
|
love.window.setMode(screenWidth * screenScale, screenHeight * screenScale, { resizable = false, vsync = true })
|
||||||
|
canvas = love.graphics.newCanvas(screenWidth, screenHeight)
|
||||||
|
end
|
||||||
|
|
||||||
|
function platform.drawStart()
|
||||||
|
love.graphics.setCanvas(canvas)
|
||||||
|
end
|
||||||
|
|
||||||
|
function platform.drawEnd()
|
||||||
|
love.graphics.setCanvas()
|
||||||
|
love.graphics.draw(canvas, 0, 0, 0, screenScale, screenScale)
|
||||||
|
end
|
||||||
|
|
||||||
|
return platform
|
||||||
6
game/platform/platform.lua
Normal file
6
game/platform/platform.lua
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
local os = love.system.getOS()
|
||||||
|
if os == 'DOS' then
|
||||||
|
return require 'platform.dos'
|
||||||
|
else
|
||||||
|
return require 'platform.native'
|
||||||
|
end
|
||||||
2
lovedos/.gitignore
vendored
Normal file
2
lovedos/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
7
run.ps1
Normal file
7
run.ps1
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
. ".\common.ps1"
|
||||||
|
|
||||||
|
$love2d = $env:LOVE2D_EXE ?? "C:/Program Files/LOVE/love.exe"
|
||||||
|
|
||||||
|
RunProgram -program $love2d -arguments $(Resolve-Path "./game")
|
||||||
13
run_dosbox.ps1
Normal file
13
run_dosbox.ps1
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
. ".\common.ps1"
|
||||||
|
|
||||||
|
$dosbox_x = $env:DOSBOX_X_EXE ?? "C:/DOSBox-X/dosbox-x.exe"
|
||||||
|
$cpu = $env:DOSBOX_X_CPU ?? "pentium_ii"
|
||||||
|
|
||||||
|
RunProgram -program $dosbox_x -arguments "-nopromptfolder",
|
||||||
|
"-fastlaunch",
|
||||||
|
"-set", "`"cputype=$cpu`"",
|
||||||
|
"-c", "`"MOUNT C $(Resolve-Path "./lovedos/") `"",
|
||||||
|
"-c", "`"MOUNT D $pwd `"",
|
||||||
|
"-c", "`"c:\love.exe d:\game `""
|
||||||
Loading…
Add table
Add a link
Reference in a new issue