From ee4762673a4d68d133bff2143a0f295d803dffcb Mon Sep 17 00:00:00 2001 From: Ivan Kuzmenko <6745157+rndtrash@users.noreply.github.com> Date: Mon, 1 Sep 2025 02:25:40 +0300 Subject: [PATCH] Initial commit --- .gitignore | 1 + .vscode/extensions.json | 5 + .vscode/settings.json | 10 ++ README.md | 18 +++ common.ps1 | 12 ++ example.env | 3 + game/3rd/lick.lua | 264 +++++++++++++++++++++++++++++++++++++ game/main.lua | 30 +++++ game/platform/dos.lua | 15 +++ game/platform/native.lua | 24 ++++ game/platform/platform.lua | 6 + lovedos/.gitignore | 2 + run.ps1 | 7 + run_dosbox.ps1 | 13 ++ 14 files changed, 410 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 common.ps1 create mode 100644 example.env create mode 100644 game/3rd/lick.lua create mode 100644 game/main.lua create mode 100644 game/platform/dos.lua create mode 100644 game/platform/native.lua create mode 100644 game/platform/platform.lua create mode 100644 lovedos/.gitignore create mode 100644 run.ps1 create mode 100644 run_dosbox.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a12ed18 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "sumneko.lua" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c2027b0 --- /dev/null +++ b/.vscode/settings.json @@ -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 +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9180eb0 --- /dev/null +++ b/README.md @@ -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. diff --git a/common.ps1 b/common.ps1 new file mode 100644 index 0000000..e234e75 --- /dev/null +++ b/common.ps1 @@ -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 +} \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..5232231 --- /dev/null +++ b/example.env @@ -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 \ No newline at end of file diff --git a/game/3rd/lick.lua b/game/3rd/lick.lua new file mode 100644 index 0000000..e74e920 --- /dev/null +++ b/game/3rd/lick.lua @@ -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 diff --git a/game/main.lua b/game/main.lua new file mode 100644 index 0000000..e24d75f --- /dev/null +++ b/game/main.lua @@ -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 diff --git a/game/platform/dos.lua b/game/platform/dos.lua new file mode 100644 index 0000000..020864e --- /dev/null +++ b/game/platform/dos.lua @@ -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 diff --git a/game/platform/native.lua b/game/platform/native.lua new file mode 100644 index 0000000..e5fada3 --- /dev/null +++ b/game/platform/native.lua @@ -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 diff --git a/game/platform/platform.lua b/game/platform/platform.lua new file mode 100644 index 0000000..392e55c --- /dev/null +++ b/game/platform/platform.lua @@ -0,0 +1,6 @@ +local os = love.system.getOS() +if os == 'DOS' then + return require 'platform.dos' +else + return require 'platform.native' +end diff --git a/lovedos/.gitignore b/lovedos/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/lovedos/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 0000000..0ccaf74 --- /dev/null +++ b/run.ps1 @@ -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") \ No newline at end of file diff --git a/run_dosbox.ps1 b/run_dosbox.ps1 new file mode 100644 index 0000000..08e9d38 --- /dev/null +++ b/run_dosbox.ps1 @@ -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 `"" \ No newline at end of file