Initial commit

This commit is contained in:
Иван Кузьменко 2025-09-01 02:25:40 +03:00
commit ee4762673a
14 changed files with 410 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"sumneko.lua"
]
}

10
.vscode/settings.json vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View file

@ -0,0 +1,2 @@
*
!.gitignore

7
run.ps1 Normal file
View 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
View 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 `""