Compare commits

...

44 commits

Author SHA1 Message Date
2445175248
Fix wget using obsolete file and HTTP Lua methods
Signed-off-by: Alessandro Proto <alex@alexdevs.me>
2024-05-27 18:03:31 +02:00
c19fb78b1f Update dependencies 2024-03-13 22:34:07 +01:00
5d0b6e864a Bump to .net 8 2024-03-13 22:33:09 +01:00
b2c3158d72 Bump version 2024-03-13 22:30:46 +01:00
1ba7185939 Replace MOTD lines with useful ones.
Add 2 new fun programs and improve Mandelbrot plotter
2024-03-13 21:00:59 +01:00
e0afdf177c Bugfix crash on invalid path chars 2023-08-31 16:24:36 +02:00
993b7f43f3
Add bios alert and colors
Signed-off-by: Alessandro Proto <alex@alexdevs.me>
2023-08-16 16:31:07 +02:00
74c9e25ad6 Disable scheduler due to bug 2023-08-15 20:28:21 +02:00
f5d6bbfaae Isolated shell environments 2023-08-15 18:04:17 +02:00
2bf4b17577 CapyOS 0.1.0 update 2023-07-28 18:16:39 +02:00
990b30b7bb Bugfix wrong directory for plugins 2023-07-28 18:15:30 +02:00
3326d0e73d Moved plugins folder to appdata.
Added config to disable plugins
2023-07-27 20:17:42 +02:00
f7d4b728f5 Add some details to appdata.xml 2023-07-24 21:52:21 +02:00
bf24c6e989 Add resource files for Linux and AppStream 2023-07-24 19:00:48 +02:00
Alessandro Proto
2b8eb54a8b Bugfix not using binary directory path for assets 2023-07-24 14:16:41 +02:00
Alessandro Proto
b85adb960b Remove dependency injection and hosting dependencies 2023-07-24 13:01:10 +02:00
d8d6e76729 Remove tools dependencies.
Add WIP Nix package
2023-07-24 06:54:27 +00:00
3fa0a73ac6 Include runtime on publish.
Bugfix hello.lua too slow.
Bump to version 1.1.0-beta.
2023-07-21 15:25:46 +02:00
fe9446e2e7 Merge branch 'main' of https://github.com/Ale32bit/Capy64 2023-07-21 14:50:56 +02:00
2f7c4fb032 Bugfixes 2023-07-21 14:48:32 +02:00
55c4f70533 8bit to 16bit PCM on audio.play string input. 2023-06-29 17:34:26 +02:00
4880fea8e3 So many small changes 2023-06-28 16:22:38 +02:00
Alessandro Proto
59bbf10e78 Add transform options to gpu.drawBuffer 2023-05-19 10:19:16 +02:00
Alessandro Proto
b44f943456 Replace debug.debug() with a dummy function 2023-05-19 08:17:07 +02:00
Alessandro Proto
efe510d0da Added firmware file to add default Lua function 2023-05-10 12:52:19 +02:00
589042ca38 Revert "Add 2 new args to machine.vibrate"
This reverts commit 86c433e966.
2023-04-28 22:10:11 +02:00
Alessandro Proto
86c433e966 Add 2 new args to machine.vibrate 2023-04-28 16:28:09 +02:00
Alessandro Proto
ad8a0dcb84 Begone TCP (not ready for it yet) 2023-04-28 16:27:57 +02:00
Alessandro Proto
74b799a0b4 Remove native event.pull and event.pullRaw 2023-04-28 11:37:45 +02:00
e152203afd "Freeze" Lua state after calling machine.reboot 2023-04-20 22:21:34 +02:00
97161f0c85 Timers now use seconds instead of ms.
Append "Lib" to all libraries class names.
2023-04-19 22:06:14 +02:00
7c799426a9 Bugfix crash upon deleting non existant /sys directory 2023-04-11 23:12:22 +02:00
Alessandro Proto
fa39689e3a Bugfix crash unauthorized delete 2023-04-11 13:26:18 +02:00
Alessandro Proto
b09ab4bdff Moved CapyOS to folder /sys 2023-04-11 13:24:11 +02:00
Alessandro Proto
756b8b35dd Replace native event.pull event.pullRaw with Lua functions 2023-04-11 11:41:56 +02:00
e520383da3 Remove cursor test 2023-04-10 12:55:23 +02:00
f5887c63c0 Increased tickrate to 30 from 20 in classic mode 2023-04-10 12:54:55 +02:00
544025290b Add alpha channel support to GPUBuffer 2023-04-08 19:31:21 +02:00
4b21daa095 Add f:lines() 2023-04-07 17:18:52 +02:00
1d28f07a7e Add support to FILE:read("n") 2023-04-06 21:14:44 +02:00
e07f79c31a Bring back CapyOS to the main repo 2023-04-05 22:17:23 +02:00
e926bd6d6b Fix #11 gpu.drawLine being offset 2023-03-21 21:26:33 +01:00
f124520a2f Hide console 2023-03-18 18:44:35 +01:00
451abe6b86 Project cleanup, welcome beta version! 2023-03-18 18:43:15 +01:00
100 changed files with 6142 additions and 789 deletions

View file

@ -33,7 +33,7 @@ jobs:
run: dotnet test --no-build --verbosity normal
- name: Publish for Windows x64
run: dotnet publish Capy64/Capy64.csproj -c Release --no-self-contained -a x64 --os win -p:PublishSingleFile=true -o capy64-windows-x64
run: dotnet publish Capy64/Capy64.csproj -c Release -a x64 --os win -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:TieredCompilation=false -o capy64-windows-x64 --self-contained
- name: Upload Windows x64 artifact
uses: actions/upload-artifact@v3.1.2
@ -64,7 +64,7 @@ jobs:
run: dotnet test --no-build --verbosity normal
- name: Publish for Linux x64
run: dotnet publish Capy64/Capy64.csproj -c Release --no-self-contained -a x64 --os linux -p:PublishSingleFile=true -o capy64-linux-x64
run: dotnet publish Capy64/Capy64.csproj -c Release -a x64 --os linux -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:TieredCompilation=false -o capy64-linux-x64 --self-contained
- name: Upload Linux x64 artifact
uses: actions/upload-artifact@v3.1.2

View file

@ -1,36 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.1.303",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-mac"
]
}
}
}

View file

@ -14,13 +14,10 @@
// limitations under the License.
using KeraLua;
using Microsoft.Extensions.DependencyInjection;
namespace Capy64.API;
public interface IComponent
{
void ConfigureServices(IServiceCollection services) { }
void LuaInit(Lua L) { }
}

View file

@ -0,0 +1,7 @@
alias ll "ls -al"
alias la "ls -a"
alias rmdir "rm -r"
alias reboot "shutdown -r"
# Comment or remove the line below to disable the MOTD
motd

View file

@ -0,0 +1,46 @@
local version = "0.1.1"
local systemDirectory = "/sys"
print("Starting CapyOS")
local term = require("term")
local fs = require("fs")
local machine = require("machine")
local nPrint = print
local function showError(err)
nPrint(err)
local x, y = term.getPos()
term.setForeground(0xff0000)
term.setPos(1, y)
term.write(err)
term.setPos(1, y + 1)
term.setForeground(0xffffff)
term.write("Press any key to continue")
coroutine.yield("key_down")
end
function os.version()
return "CapyOS " .. version
end
term.setPos(1, 1)
term.write(machine.version())
term.setPos(1, 3)
local files = fs.list(fs.combine(systemDirectory, "boot/autorun"))
for i = 1, #files do
local func, err = loadfile(fs.combine(systemDirectory, "boot/autorun", files[i]))
if not func then
showError(err)
break
end
local ok, err = pcall(func)
if not ok then
showError(debug.traceback(err))
break
end
end

View file

@ -0,0 +1,28 @@
local argparser = require("argparser")
local args, options = argparser.parse(...)
if options.l or options.list then
for alias, value in pairs(shell.aliases) do
print(string.format("%s = \"%s\"", alias, value))
end
return
end
local alias = args[1]
if not alias or options.h or options.help then
print("Usage: alias [option...] <alias> [command]")
print("Options:")
print(" -l --list: List aliases")
return false
end
local command = table.pack(select(2, ...))
if #command == 0 then
shell.aliases[alias] = nil
return
end
shell.aliases[alias] = table.concat(command, " ")

View file

@ -0,0 +1,4 @@
local scheduler = require("scheduler")
scheduler.spawn(function()
shell.run(arg.string)
end)

View file

@ -0,0 +1,9 @@
local fs = require("fs")
local args = {...}
local path = shell.resolve(args[1])
local f<close> = fs.open(path, "r")
print(f:read("a"))
f:close()

View file

@ -0,0 +1,17 @@
local args = {...}
local fs = require("fs")
local dir = args[1]
if not dir then
dir = shell.homePath
end
dir = shell.resolve(dir)
if not fs.isDir(dir) then
error("No such directory: " .. dir, 0)
return false
end
shell.setDir(dir)

View file

@ -0,0 +1,5 @@
local term = require("term")
local colors = require("colors")
term.setBackground(colors.black)
term.clear()
term.setPos(1, 1)

View file

@ -0,0 +1,4 @@
local argparser = require("argparser")
local args, options = argparser.parse(...)
print(table.concat(args, " "))

View file

@ -0,0 +1 @@
shell.exit()

View file

@ -0,0 +1,40 @@
local gpu = require("gpu")
local event = require("event")
local donuts = {}
local limit = 100
local w, h = gpu.getSize()
local function insert()
local donut = {
x = math.random(-20, w + 20),
y = math.random(-20, h + 20),
d = math.random() * math.pi*2,
dir = math.random(0, 1),
c = math.random(0xffffff),
life = math.random(100, 1000),
}
table.insert(donuts, donut)
end
while true do
if #donuts < limit then
insert()
end
gpu.clear(0)
for k, donut in ipairs(donuts) do
if donut.life <= 0 then
table.remove(donuts, k)
end
local doReverse = math.random(0, 1000) > 950
donut.x = donut.x + math.cos(donut.d) * 4
donut.y = donut.y + math.sin(donut.d) * 4
donut.d = donut.d + (donut.dir == 1 and 0.05 or -0.05)
gpu.drawCircle(donut.x, donut.y, 20, donut.c, 10)
if doReverse then
donut.dir = donut.dir == 1 and 0 or 1
end
donut.life = donut.life - 1
end
event.push("donuts")
event.pull("donuts")
end

View file

@ -0,0 +1,98 @@
-- Mandelbrot in Capy64
local gpu = require("gpu")
local timer = require("timer")
local event = require("event")
local term = require("term")
-- lower = closer = slower
local scale = 4
-- higher = more detailed = slower
local iterations = 100
local pscale = scale
local w, h = gpu.getSize()
local px = pscale / w
local colorUnit = math.floor(0xffffff / iterations)
-- todo: make it interactive
local dx, dy = 0, 0
local cx, cy = math.floor(w / 2), math.floor(h / 2)
-- z = z^2 + c
local function mandelbrot(zr, zi, cr, ci)
return zr ^ 2 - zi ^ 2 + cr,
2 * zr * zi + ci
end
local function iter(cr, ci)
local zr, zi = 0, 0
for i = 1, iterations do
zr, zi = mandelbrot(zr, zi, cr, ci)
if math.abs(zr) >= (pscale >= 2 and pscale or 2) or math.abs(zi) >= (pscale >= 2 and pscale or 2) then
return false, colorUnit, i
end
end
return true, 0, iterations
end
local function draw()
local size = w * h
local canvas = { string.unpack(("B"):rep(size), ("\0"):rep(size)) }
canvas[#canvas] = nil
for y = 0, h - 1 do
for x = 0, w - 1 do
local _, _, i = iter((x - cx + dx * pscale) * px, (y - cy + dy * pscale) * px)
canvas[y * w + x] = colorUnit * (iterations - i)
end
end
local buffer <close> = gpu.bufferFrom(canvas, w, h)
gpu.setBuffer(buffer)
end
-- no idea why it's needed
timer.sleep(0)
draw()
local tw, th = term.getSize()
while true do
term.setPos(1, th)
term.setBackground(0)
term.setForeground(0xffffff)
term.write("X: " .. dx .. "; Y: " .. dy .. "; S: " .. pscale .. "; " .. px .. "!")
local ev = { event.pull("key_down") }
if ev[1] == "key_down" then
local key = ev[3]
if key == "up" then
dy = dy - 10 / pscale
elseif key == "down" then
dy = dy + 10 / pscale
elseif key == "right" then
dx = dx + 10 / pscale
elseif key == "left" then
dx = dx - 10 / pscale
elseif key == "enter" then
draw()
elseif key == "page_down" then
pscale = pscale * 1.25
dx = dx * pscale
dy = dy * pscale
elseif key == "page_up" then
pscale = pscale / 1.25
dx = dx / pscale
dy = dy / pscale
elseif key == "r" then
pscale = scale
dx = 0
dy = 0
end
end
px = pscale / w
end

View file

@ -0,0 +1,73 @@
local gpu = require("gpu")
local timer = require("timer")
local event = require("event")
local colors = require("colors")
local parallel = require("parallel")
local melts = 2 ^ 12
local function contains(arr, val)
for k, v in ipairs(arr) do
if v == val then
return true
end
end
return false
end
local function melt()
local w, h = gpu.getSize()
local x, y = 0, 0
while true do
local buffer <close> = gpu.getBuffer()
for i = 1, melts do
local nx = math.random(x, w)
local ny = math.random(y, h)
local c = buffer[ny * w + nx]
buffer[(ny + 1) * w + nx] = c
end
gpu.setBuffer(buffer)
timer.delay(0):await()
end
end
local function draw()
local ox, oy
while true do
local ev, b, x, y = event.pull("mouse_move", "mouse_down")
if ev == "mouse_down" then
if b == 1 then
ox = x
oy = y
end
elseif ev == "mouse_move" then
if contains(b, 1) then
gpu.plot(x, y, colors.red)
gpu.drawLine(x, y, ox, oy, colors.red)
ox, oy = x, y
end
end
end
end
local function random()
local w, h = gpu.getSize()
while true do
for i = 1, 24 do
gpu.drawString(
math.random(-7, w),
math.random(-13, h),
math.random(0, 0xffffff),
string.char(math.random(32, 127))
)
end
timer.delay(0.1):await()
end
end
parallel.waitForAny(draw, melt, random)

View file

@ -0,0 +1,209 @@
local event = require("event")
local gpu = require("gpu")
local colors = require("colors")
local term = require("term")
local timer = require("timer")
local w, h = gpu.getSize()
local tw, th = term.getSize()
local selectedColor = 1
local thickness = 4
local canvasW, canvasH = term.toRealPos(tw - 1, th + 1)
canvasW = canvasW - 3
local size = canvasW * canvasH
local canvas = {string.unpack(("B"):rep(size), ("\0"):rep(size))}
canvas[#canvas] = nil
local function drawCircle(buffer, x, y, radius, color)
radius = math.max(0, radius)
if radius == 0 then
buffer[x + buffer.width * y] = color
return
end
local width = buffer.width
local height = buffer.height
local index = function(x, y)
return y * width + x
end
local isValid = function(x, y)
return x >= 0 and x < width and y >= 0 and y < height
end
local setPixel = function(x, y, color)
if isValid(x, y) then
buffer[index(x, y)] = color
end
end
local drawFilledCirclePoints = function(cx, cy, x, y)
for dx = -x, x do
for dy = -y, y do
setPixel(cx + dx, cy + dy, color)
end
end
end
local drawCircleBresenham = function(cx, cy, radius)
local x = 0
local y = radius
local d = 3 - 2 * radius
drawFilledCirclePoints(cx, cy, x, y)
while y >= x do
x = x + 1
if d > 0 then
y = y - 1
d = d + 4 * (x - y) + 10
else
d = d + 4 * x + 6
end
drawFilledCirclePoints(cx, cy, x, y)
end
end
drawCircleBresenham(x, y, radius)
end
local function drawLine(buffer, x0, y0, x1, y1, color, thickness)
local width = canvasW
local height = canvasH
local index = function(x, y)
return y * width + x
end
local isValid = function(x, y)
return x >= 0 and x < width and y >= 0 and y < height
end
local setPixel = function(x, y)
if isValid(x, y) then
buffer[index(x, y)] = color
end
end
local drawLineBresenham = function()
local i = 0
local dx = math.abs(x1 - x0)
local dy = math.abs(y1 - y0)
local sx = x0 < x1 and 1 or -1
local sy = y0 < y1 and 1 or -1
local err = dx - dy
local majorAxis = dx > dy
while x0 ~= x1 or y0 ~= y1 do
for i = 0, thickness - 1 do
if majorAxis then
setPixel(x0, y0 + i)
else
setPixel(x0 + i, y0)
end
end
local err2 = 2 * err
if err2 > -dy then
err = err - dy
x0 = x0 + sx
end
if err2 < dx then
err = err + dx
y0 = y0 + sy
end
if i % 1024 == 0 then
--event.push("paint")
--event.pull("paint")
--timer.sleep(0)
end
end
end
drawLineBresenham()
end
local function drawUI()
term.setBackground(0)
term.clear()
for y = 1, 16 do
term.setPos(tw - 1, y)
term.setBackground(0)
term.setForeground(colors[y])
if selectedColor == y then
term.setBackground(colors[y])
term.write(" ")
else
term.write("##")
end
end
term.setPos(tw - 1, 17)
if selectedColor == 0 then
term.setBackground(colors.white)
term.setForeground(0)
else
term.setBackground(0)
term.setForeground(colors.white)
end
term.write("XX")
term.setPos(tw - 1, 18)
term.setBackground(colors.black)
term.setForeground(colors.white)
term.write(thickness)
gpu.drawLine(canvasW + 1, 0, canvasW, canvasH, colors.gray, 2)
local b<close> = gpu.bufferFrom(canvas, canvasW, canvasH)
gpu.drawBuffer(b, 0, 0, {
source = {
0, 0, canvasW, canvasH
}
})
end
local function contains(arr, val)
for i, v in ipairs(arr) do
if v == val then
return true
end
end
return false
end
local oldX, oldY
while true do
drawUI()
local ev, b, x, y = event.pull("mouse_down", "mouse_up", "mouse_move", "mouse_scroll")
local tx, ty = term.fromRealPos(x, y)
if ev == "mouse_up" then
if x >= canvasW then
if ty <= 16 then
selectedColor = ty
elseif ty == 17 then
selectedColor = 0
end
end
oldX, oldY = nil, nil
elseif ev == "mouse_down" or (ev == "mouse_move" and contains(b, 1)) then
if x < canvasW and y < canvasH then
--canvas[x + y * canvasW] = colors[selectedColor] or 0
--drawCircle(canvas, x, y, thickness - 2, colors[selectedColor])
drawLine(canvas, x, y, oldX or x, oldY or y, colors[selectedColor] or 0, thickness)
--gpu.drawLine(x, y, oldX or x, oldY or y, colors[selectedColor] or 0)
--canvas = gpu.getBuffer()
oldX, oldY = x, y
end
elseif ev == "mouse_scroll" then
local x, y, b = b, x, y
local tx, ty = term.fromRealPos(x, y)
if x >= canvasW and ty == 18 then
thickness = math.min(99, math.max(0, thickness - b))
end
end
end

View file

@ -0,0 +1,23 @@
local timer = require("timer")
local colors = require("colors")
local term = require("term")
local function slowPrint(text, delay)
for i = 1, #text do
local ch = text:sub(i, i)
io.write(ch)
timer.sleep(delay)
end
print()
end
local args = {...}
local text = "Hello, World!"
if #args > 0 then
text = table.concat(args, " ")
end
local color = colors[math.random(1, #colors)]
term.setForeground(color)
slowPrint(text, 0.05)

View file

@ -0,0 +1,11 @@
local fs = require("fs")
local helpPath = "/sys/share/help"
local topicName = arg[1] or "index"
if not fs.exists(fs.combine(helpPath, topicName)) then
print(string.format("Topic \"%s\" not found.", topicName))
return false
end
shell.run("/sys/bin/less.lua", fs.combine(helpPath, topicName))

View file

@ -0,0 +1,78 @@
local term = require("term")
local keys = require("keys")
local event = require("event")
local fs = require("fs")
local timer = require("timer")
local colors = require("colors")
local filename = shell.resolve(arg[1])
local f<close> = fs.open(filename, "r")
local lines = {}
local lineMax = 0
for line in f:lines() do
table.insert(lines, line)
lineMax = math.max(lineMax, #line)
end
f:close()
local width, height = term.getSize()
height = height - 1
local posx, posy = 0, 0
local function redraw()
term.clear()
term.setForeground(colors.white)
for i = 1, height do
if i + posy > #lines then
break
end
term.setPos(-posx + 1, i)
term.write(lines[i + posy])
end
term.setForeground(colors.yellow)
term.setPos(1, height + 1)
term.write("Use arrow keys to move or press Q to exit.")
end
while true do
redraw()
local _, key = event.pull("key_down")
if key == keys.enter or key == keys.down then
posy = posy + 1
elseif key == keys.up then
posy = posy - 1
elseif key == keys.right then
posx = posx + 1
elseif key == keys.left then
posx = posx - 1
elseif key == keys.q or key == keys.escape then
-- Clear event queue
timer.sleep(0)
term.clear()
term.setPos(1, 1)
break
end
if posy > #lines - height then
posy = #lines - height
end
if posy < 0 then
posy = 0
end
if posx + width > lineMax + 1 then
posx = lineMax - width + 1
end
if posx < 0 then
posx = 0
end
end

View file

@ -0,0 +1,88 @@
local fs = require("fs")
local term = require("term")
local colors = require("colors")
local argparser = require("argparser")
local theme = {
directory = colors.lightBlue,
file = colors.white,
lua = colors.yellow,
}
local function humanizeBytes(n)
local prefixes = {
[0] = "",
"k",
"M",
"G",
"T",
}
local block = 1024
local prefixIndex = 0
while n >= block do
n = n / 1024
prefixIndex = prefixIndex + 1
end
return string.format("%.0f%s", n, prefixes[prefixIndex])
end
local args, options = argparser.parse(...)
if options.h or options.help then
print("Usage: ls [option...] [path]")
print("List files (current directory by default)")
print("Options:")
print(" -a: Include hidden files")
print(" -l: Use long listing format")
return
end
local path = shell.getDir()
if args[1] then
path = shell.resolve(args[1])
end
if not fs.isDir(path) then
error("No such directory: " .. path, 0)
return false
end
local entries = fs.list(path)
if options.l then
print(string.format("total %d", #entries))
end
local printed = 0
for i, entry in ipairs(entries) do
if entry:sub(1, 1) ~= "." or options.a then
printed = printed + 1
local attributes = fs.attributes(fs.combine(path, entry))
local size = humanizeBytes(attributes.size)
local date = os.date("%x %H:%m", attributes.modified // 1000)
local entryType
if attributes.isDirectory then
entryType = "directory"
else
entryType = "file"
if string.match(entry, "%.lua$") then
entryType = "lua"
end
end
if options.l then
term.setForeground(colors.white)
term.write(string.format("%s %5s %s ", attributes.isDirectory and "d" or "-", size, date))
end
term.setForeground(theme[entryType])
io.write(entry)
io.write(options.l and "\n" or "\t")
end
end
if not options.l and printed > 0 then
print()
end

View file

@ -0,0 +1,98 @@
local term = require("term")
local io = require("io")
local colors = require("colors")
local argparser = require("argparser")
local tableutils = require("tableutils")
local args, options = argparser.parse(...)
local function evaluate(str, env, chunkname)
chunkname = chunkname or "=lua"
local nForcePrint = 0
local func, e = load(str, chunkname, "t", env)
local func2 = load("return " .. str, chunkname, "t", env)
if not func then
if func2 then
func = func2
e = nil
nForcePrint = 1
end
else
if func2 then
func = func2
end
end
if func then
local tResults = table.pack(pcall(func))
if tResults[1] then
local n = 1
while n < tResults.n or n <= nForcePrint do
local value = tResults[n + 1]
print(tableutils.pretty(value))
n = n + 1
end
else
io.stderr.print(tResults[2])
return false
end
else
io.stderr.print(e)
return false
end
return true
end
local function createEnvironment()
return setmetatable({}, { __index = _ENV })
end
local function loadPackages(env)
for k, v in pairs(package.loaded) do
env[k] = v
end
end
if options.e then
local env = createEnvironment()
loadPackages(env)
return evaluate(table.concat(args, " "), env)
end
if #args > 0 then
print("This is an interactive Lua prompt.")
print("To run a lua program, just type its name.")
return
end
--local pretty = require "cc.pretty"
local bRunning = true
local tCommandHistory = {}
local tEnv = createEnvironment()
tEnv.exit = setmetatable({}, {
__tostring = function() return "Call exit() to exit." end,
__call = function() bRunning = false end,
})
loadPackages(tEnv)
term.setForeground(colors.yellow)
print(_VERSION .. " interactive prompt")
print("Call exit() to exit.")
term.setForeground(colors.white)
while bRunning do
term.setForeground(colors.yellow)
io.write("> ")
term.setForeground(colors.white)
local s = io.read(nil, tCommandHistory)
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s)
end
evaluate(s, tEnv)
end

View file

@ -0,0 +1,14 @@
local fs = require("fs")
local args = { ... }
if #args == 0 then
print("Usage: mkdir <directory>")
return
end
local dir = shell.resolve(args[1])
if fs.exists(dir) then
error("Path already exists", 0)
end
fs.makeDir(dir)

View file

@ -0,0 +1,20 @@
local fs = require("fs")
local date = os.date("*t")
if date.month == 4 and date.day == 28 then
print("Ed Balls")
return
end
local motdList = {}
local f<close> = fs.open("/sys/share/motd.txt", "r")
for line in f:lines() do
table.insert(motdList, line)
end
f:close()
local motdIndex = math.random(1, #motdList)
print(motdList[motdIndex])

View file

@ -0,0 +1,16 @@
local fs = require("fs")
local argparser = require("argparser")
local args, options = argparser.parse(...)
if not args[1] or not args[2] or options.h or options.help then
print("Usage: mv [option...] <source> <target>")
print("Options:")
print(" -h --help: Display help")
return
end
local source = shell.resolve(args[1])
local destination = shell.resolve(args[2])
fs.move(source, destination)

View file

@ -0,0 +1,10 @@
local fs = require("fs")
local programs = fs.list("/sys/bin", function(name, attr)
return not attr.isDirectory
end)
for i, v in ipairs(programs) do
programs[i] = string.gsub(v, "%.lua$", "")
end
print(table.concat(programs, " "))

View file

@ -0,0 +1 @@
print(shell.getDir())

View file

@ -0,0 +1,16 @@
local fs = require("fs")
local argparser = require("argparser")
local args, options = argparser.parse(...)
if not args[1] or options.h or options.help then
print("Usage: rm [option...] <path>")
print("Options:")
print(" -r --recursive: Delete non-empty directories")
print(" -h --help: Display help")
return
end
local file = shell.resolve(args[1])
fs.delete(file, options.recursive or options.r)

View file

@ -0,0 +1,175 @@
local term = require("term")
local colors = require("colors")
local fs = require("fs")
local machine = require("machine")
local argparser = require("argparser")
local scheduler = require("scheduler")
local createPackageEnvironment = require("shell.package")
local useScheduler = false
local exit = false
local parentShell = shell
local isStartupShell = parentShell == nil
local shell = {}
shell.path = parentShell and parentShell.path or "./?;./?.lua;/bin/?.lua;/sys/bin/?.lua"
shell.homePath = parentShell and parentShell.home or "/home"
shell.aliases = parentShell and parentShell.aliases or {}
local currentDir = parentShell and parentShell.getDir() or shell.homePath
local function buildEnvironment(command, filepath, args, argf)
local arg = { table.unpack(args, 2) }
arg[0] = command
arg.string = argf
local envPackage = createPackageEnvironment(filepath)
envPackage.loaded.scheduler = scheduler
return setmetatable({
shell = shell,
arg = arg,
scheduler = scheduler,
}, { __index = envPackage.loaded._G })
end
function shell.getDir()
return currentDir
end
function shell.setDir(path)
currentDir = path
end
function shell.resolve(path)
if path:sub(1, 1) == "/" then
return fs.combine("", path)
end
if path:sub(1, 1) == "~" then
return fs.combine(shell.homePath, path)
end
return fs.combine(currentDir, path)
end
function shell.resolveProgram(path)
if path:sub(1, 1) == "/" then
return shell.resolve(path)
end
for seg in shell.path:gmatch("[^;]+") do
local resolved = shell.resolve(seg:gsub("%?", path))
if fs.exists(resolved) and not fs.isDir(resolved) then
return resolved
end
end
end
function shell.run(...)
local args = argparser.tokenize(...)
local argf = table.concat({...}, " ")
local command = args[1]
argf = argf:sub(#command + 2)
local path = shell.resolveProgram(command)
if not path then
if shell.aliases[command] then
return shell.run(shell.aliases[command], select(2, table.unpack(args)))
else
io.stderr.print("Command not found: " .. command)
return false
end
end
local env = buildEnvironment(command, path, args, argf)
local func, err = loadfile(path, "t", env)
if not func then
io.stderr.print(err)
return false
end
local ok, err
local function run()
ok, err = pcall(func, table.unpack(args, 2))
end
if useScheduler then
local programTask, yielded = scheduler.spawn(run)
if yielded then
coroutine.yield("scheduler_task_end")
end
else
run()
end
if not ok then
io.stderr.print(err)
return false
end
return true
end
function shell.exit()
exit = true
end
if not fs.exists(shell.homePath) then
fs.makeDir(shell.homePath)
end
term.setForeground(colors.white)
term.setBackground(colors.black)
if isStartupShell then
if fs.exists(fs.combine(shell.homePath, ".shrc")) then
local f <close> = fs.open(fs.combine(shell.homePath, ".shrc"), "r")
for line in f:lines() do
if line:match("%S") and not line:match("^%s-#") then
shell.run(line)
end
end
f:close()
end
end
local history = {}
local lastExecSuccess = true
while not exit do
machine.setRPC(os.version(), "On shell")
term.setBackground(colors.black)
term.setForeground(colors.white)
io.write(":")
term.setForeground(colors.lightBlue)
if currentDir == shell.homePath then
io.write("~")
else
io.write(currentDir)
end
if lastExecSuccess then
term.setForeground(colors.yellow)
else
term.setForeground(colors.red)
end
io.write("$ ")
term.setForeground(colors.white)
local line = io.read(nil, history)
if line:match("%S") and history[#history] ~= line then
table.insert(history, line)
end
if line:match("%S") then
machine.setRPC(os.version(), "Running: " .. line)
lastExecSuccess = shell.run(line)
end
end

View file

@ -0,0 +1,33 @@
local machine = require("machine")
local scheduler = require("scheduler")
local argparser = require("argparser")
local args, options = argparser.parse(...)
if options.h or options.help then
print("Usage: shutdown [option...]")
print("Shutdown or restart Capy64.")
print("Options:")
print(" -s --shutdown: Shutdown and exit Capy64. (default)")
print(" -r --reboot: Restart Capy64.")
print(" -t --time: Time to wait in seconds. (\"now\" is 0 seconds, default)")
return
end
local time = 0
if options.t or options.time then
time = options.t or options.time
end
if time == "now" then
time = 0
else
time = tonumber(time)
if not time then
error("Invalid time option: " .. (options.t or options.time), 0)
end
end
scheduler.ipc(1, "power", {
reboot = options.r or options.reboot,
time = time,
})

View file

@ -0,0 +1,2 @@
local machine = require("machine")
print(string.format("%s @ %s - %s", os.version(), machine.version(), _VERSION))

View file

@ -0,0 +1,36 @@
local http = require("http")
local fs = require("fs")
local args = { ... }
if not http then
error("HTTP is not enabled", 0)
end
if #args == 0 then
print("Usage: wget <url> [outputPath]")
return false
end
local outputName = args[2] or fs.getName(args[1])
local outputPath = shell.resolve(outputName)
if not http.checkURL(args[1]) then
error("Invalid URL", 0)
end
print("Connecting...")
local response, err = http.get(args[1], nil, {
binary = true,
})
if not response then
error(err, 0)
end
local file <close> = fs.open(outputPath, "wb")
file:write(response.content:read("a"))
file:close()
response.content:close()
print("Downloaded to " .. outputPath)

View file

@ -0,0 +1,3 @@
package.path = "/lib/?.lua;/lib/?/init.lua;/sys/lib/?.lua;/sys/lib/?/init.lua;" .. package.path
_G.io = require("io")

View file

@ -0,0 +1,15 @@
function _G.print(...)
local args = { ... }
local size = #args
local lines = 0
for n, v in ipairs(args) do
local s = tostring(v)
if n < size then
s = s .. "\t"
end
lines = lines + io.write(s)
end
lines = lines + io.write("\n")
return lines
end

View file

@ -0,0 +1,28 @@
local fs = require("fs")
local expect = require("expect").expect
local fsList = fs.list
function fs.list(path, filter)
expect(1, path, "string")
expect(2, filter, "nil", "function")
if not fs.isDir(path) then
error("directory not found", 2)
end
local list = fsList(path)
if not filter then
return list
end
local filteredList = {}
for i = 1, #list do
local attributes = fs.attributes(fs.combine(path, list[i]))
if filter(list[i], attributes) then
table.insert(filteredList, list[i])
end
end
return filteredList
end

View file

@ -0,0 +1,77 @@
local http = require("http")
local event = require("event")
local expect = require("expect").expect
function http.request(url, body, headers, options)
expect(1, url, "string")
expect(2, body, "string", "nil")
expect(3, headers, "table", "nil")
expect(4, options, "table", "nil")
if not http.checkURL(url) then
return nil, "Invalid URL"
end
local task<close> = http.requestAsync(url, body, headers, options)
return task:await()
end
function http.get(url, headers, options)
expect(1, url, "string")
expect(2, headers, "table", "nil")
expect(3, options, "table", "nil")
return http.request(url, nil, headers, options)
end
function http.post(url, body, headers, options)
expect(1, url, "string")
expect(2, body, "string", "nil")
expect(3, headers, "table", "nil")
expect(4, options, "table", "nil")
return http.request(url, body, headers, options)
end
local WebSocketHandle
local function buildWebsocketHandle(handle)
if not handle then
return nil
end
if not WebSocketHandle then
WebSocketHandle = getmetatable(handle) or { __index = {} }
function WebSocketHandle.__index:close()
self:closeAsync()
local _, id
repeat
_, id = event.pull("websocket_close")
until id == self:getRequestID()
end
function WebSocketHandle.__index:receive()
local _, id, par
repeat
_, id, par = event.pull("websocket_message")
until id == self:getRequestID()
return par
end
end
return handle
end
function http.websocket(url, headers)
expect(1, url, "string")
expect(2, headers, "table", "nil")
if not http.checkURL(url) then
return nil, "Invalid URL"
end
local task<close> = http.websocketAsync(url, headers)
local client, err = task:await()
return buildWebsocketHandle(client), err
end

View file

@ -0,0 +1,13 @@
local timer = require("timer")
local event = require("event")
local expect = require("expect").expect
local range = require("expect").range
function timer.sleep(n)
expect(1, n, "number")
local timerId = timer.start(n)
repeat
local _, par = event.pull("timer")
until par == timerId
end

View file

@ -0,0 +1,47 @@
local machine = require("machine")
local scheduler = require("scheduler")
local term = require("term")
local colors = require("colors")
term.setForeground(0x59c9ff)
term.setBackground(colors.black)
term.clear()
term.setPos(1, 1)
term.write(os.version())
term.setPos(1, 2)
local function spawnShell()
return scheduler.spawn(loadfile("/sys/bin/shell.lua"))
end
local function main()
local shellTask = spawnShell()
while true do
local ev = {coroutine.yield()}
if ev[1] == "ipc_message" then
local sender = ev[2]
local call = ev[3]
if call == "power" then
local options = ev[4]
--todo: handle time and cancels
if options.reboot then
machine.reboot()
else
machine.shutdown()
end
end
elseif ev[1] == "scheduler_task_end" then
if ev[2].pid == shellTask.pid then
if not ev[3] then
io.stderr.print(ev[4])
end
shellTask = spawnShell()
end
end
end
end
scheduler.spawn(main)
scheduler.init()

View file

@ -0,0 +1 @@
require("machine").shutdown()

View file

@ -0,0 +1,91 @@
local argparser = {}
function argparser.tokenize(...)
local input = table.concat(table.pack(...), " ")
local tokens = {}
-- surely there must be a better way
local quoted = false
local escaped = false
local current = ""
for i = 1, #input do
local char = input:sub(i, i)
if escaped then
escaped = false
current = current .. char
else
if char == "\\" then
escaped = true
elseif char == "\"" then
if quoted then
-- close quote
table.insert(tokens, current)
current = ""
end
quoted = not quoted
elseif char == " " and not quoted then
if #current > 0 then
table.insert(tokens, current)
end
current = ""
else
current = current .. char
end
end
end
if current ~= "" then
table.insert(tokens, current)
end
return tokens
end
function argparser.parse(...)
local tokens = { ... }
local args = {}
local options = {}
local ignoreOptions = false
for i = 1, #tokens do
local token = tokens[i]
if not ignoreOptions then
if token == "--" then
ignoreOptions = true
elseif token:sub(1, 2) == "--" then
local opt, value = token:match("%-%-(.+)=(.+)")
if not opt then
opt = token:sub(3)
if opt:sub(-1) == "=" then
-- next token is value
value = tokens[i + 1]
opt = opt:sub(1, -2)
options[opt] = value
i = i + 1
else
options[opt] = true
end
else
options[opt] = value
end
elseif token:sub(1, 1) == "-" then
local opts = token:sub(2)
for j = 1, #opts do
options[opts:sub(j, j)] = true
end
else
if #token > 0 then
table.insert(args, token)
end
end
else
if #token > 0 then
table.insert(args, token)
end
end
end
return args, options
end
return argparser

View file

@ -0,0 +1,94 @@
local expect = require("expect")
local palette = {
{
"white",
0xf0f0f0
},
{
"orange",
0xf2b233
},
{
"magenta",
0xe57fd8
},
{
"lightBlue",
0x99b2f2
},
{
"yellow",
0xdede6c
},
{
"lime",
0x7fcc19
},
{
"pink",
0xf2b2cc
},
{
"gray",
0x4c4c4c
},
{
"lightGray",
0x999999
},
{
"cyan",
0x4c99b2
},
{
"purple",
0xb266e5
},
{
"blue",
0x3366cc
},
{
"brown",
0x7f664c
},
{
"green",
0x57a64e
},
{
"red",
0xcc4c4c
},
{
"black",
0x111111
}
}
local colors = {}
for k, v in ipairs(palette) do
colors[v[1]] = v[2]
colors[k] = v[2]
end
function colors.packRGB(r, g, b)
expect(1, r, "number")
expect(2, g, "number")
expect(3, b, "number")
return (r << 16) +
(g << 8) +
b
end
function colors.unpackRGB(rgb)
expect(1, rgb, "number")
return (rgb >> 16) & 0xff,
(rgb >> 8) & 0xff,
rgb & 0xff
end
return colors;

View file

@ -0,0 +1,52 @@
-- Credits: https://github.com/Ocawesome101/recrafted
-- cc.expect
local _expect = {}
local function checkType(index, valueType, value, ...)
local expected = table.pack(...)
local isType = false
for i = 1, expected.n, 1 do
if type(value) == expected[i] then
isType = true
break
end
end
if not isType then
error(string.format("bad %s %s (%s expected, got %s)", valueType,
index, table.concat(expected, " or "), type(value)), 3)
end
return value
end
function _expect.expect(index, value, ...)
return checkType(("#%d"):format(index), "argument", value, ...)
end
function _expect.field(tbl, index, ...)
_expect.expect(1, tbl, "table")
_expect.expect(2, index, "string")
return checkType(("%q"):format(index), "field", tbl[index], ...)
end
function _expect.range(num, min, max)
_expect.expect(1, num, "number")
_expect.expect(2, min, "number", "nil")
_expect.expect(3, max, "number", "nil")
min = min or -math.huge
max = max or math.huge
if num < min or num > max then
error(("number outside of range (expected %d to be within %d and %d")
:format(num, min, max), 2)
end
end
setmetatable(_expect, { __call = function(_, ...)
return _expect.expect(...)
end })
return _expect

View file

@ -0,0 +1,616 @@
local expect = require("expect").expect
local event = require("event")
local term = require("term")
local keys = require("keys")
local machine = require("machine")
local io = {}
function io.write(sText)
sText = tostring(sText)
local w, h = term.getSize()
local x, y = term.getPos()
local nLinesPrinted = 0
local function newLine()
if y + 1 <= h then
term.setPos(1, y + 1)
else
term.setPos(1, h)
term.scroll(1)
end
x, y = term.getPos()
nLinesPrinted = nLinesPrinted + 1
end
-- Print the line with proper word wrapping
sText = tostring(sText)
while #sText > 0 do
local whitespace = string.match(sText, "^[ \t]+")
if whitespace then
-- Print whitespace
term.write(whitespace)
x, y = term.getPos()
sText = string.sub(sText, #whitespace + 1)
end
local newline = string.match(sText, "^\n")
if newline then
-- Print newlines
newLine()
sText = string.sub(sText, 2)
end
local text = string.match(sText, "^[^ \t\n]+")
if text then
sText = string.sub(sText, #text + 1)
if #text > w then
-- Print a multiline word
while #text > 0 do
if x > w then
newLine()
end
term.write(text)
text = string.sub(text, w - x + 2)
x, y = term.getPos()
end
else
-- Print a word normally
if x + #text - 1 > w then
newLine()
end
term.write(text)
x, y = term.getPos()
end
end
end
return nLinesPrinted
end
function io.read(_sReplaceChar, _tHistory, _fnComplete, _sDefault)
expect(1, _sReplaceChar, "string", "nil")
expect(2, _tHistory, "table", "nil")
expect(3, _fnComplete, "function", "nil")
expect(4, _sDefault, "string", "nil")
term.setBlink(true)
local sLine
if type(_sDefault) == "string" then
sLine = _sDefault
else
sLine = ""
end
local nHistoryPos
local nPos, nScroll = #sLine, 0
if _sReplaceChar then
_sReplaceChar = string.sub(_sReplaceChar, 1, 1)
end
local tCompletions
local nCompletion
local function recomplete()
if _fnComplete and nPos == #sLine then
tCompletions = _fnComplete(sLine)
if tCompletions and #tCompletions > 0 then
nCompletion = 1
else
nCompletion = nil
end
else
tCompletions = nil
nCompletion = nil
end
end
local function uncomplete()
tCompletions = nil
nCompletion = nil
end
local w = term.getSize()
local sx = term.getPos()
local function redraw(_bClear)
local cursor_pos = nPos - nScroll
if sx + cursor_pos >= w then
-- We've moved beyond the RHS, ensure we're on the edge.
nScroll = sx + nPos - w
elseif cursor_pos < 0 then
-- We've moved beyond the LHS, ensure we're on the edge.
nScroll = nPos
end
local _, cy = term.getPos()
term.setPos(sx, cy)
local sReplace = _bClear and " " or _sReplaceChar
if sReplace then
term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0)))
else
term.write(string.sub(sLine, nScroll + 1))
end
if nCompletion then
local sCompletion = tCompletions[nCompletion]
local oldText, oldBg
if not _bClear then
oldText = term.getTextColor()
oldBg = term.getBackgroundColor()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.gray)
end
if sReplace then
term.write(string.rep(sReplace, #sCompletion))
else
term.write(sCompletion)
end
if not _bClear then
term.setTextColor(oldText)
term.setBackgroundColor(oldBg)
end
end
term.setPos(sx + nPos - nScroll, cy)
end
local function clear()
redraw(true)
end
recomplete()
redraw()
local function acceptCompletion()
if nCompletion then
-- Clear
clear()
-- Find the common prefix of all the other suggestions which start with the same letter as the current one
local sCompletion = tCompletions[nCompletion]
sLine = sLine .. sCompletion
nPos = #sLine
-- Redraw
recomplete()
redraw()
end
end
while true do
local sEvent, param, param1, param2 = event.pull()
if sEvent == "char" then
-- Typed key
clear()
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
nPos = nPos + 1
recomplete()
redraw()
elseif sEvent == "paste" then
-- Pasted text
clear()
sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
nPos = nPos + #param
recomplete()
redraw()
elseif sEvent == "key_down" then
if param == keys.enter or param == keys.numPadEnter then
-- Enter/Numpad Enter
if nCompletion then
clear()
uncomplete()
redraw()
end
break
elseif param == keys.left then
-- Left
if nPos > 0 then
clear()
nPos = nPos - 1
recomplete()
redraw()
end
elseif param == keys.right then
-- Right
if nPos < #sLine then
-- Move right
clear()
nPos = nPos + 1
recomplete()
redraw()
else
-- Accept autocomplete
acceptCompletion()
end
elseif param == keys.up or param == keys.down then
-- Up or down
if nCompletion then
-- Cycle completions
clear()
if param == keys.up then
nCompletion = nCompletion - 1
if nCompletion < 1 then
nCompletion = #tCompletions
end
elseif param == keys.down then
nCompletion = nCompletion + 1
if nCompletion > #tCompletions then
nCompletion = 1
end
end
redraw()
elseif _tHistory then
-- Cycle history
clear()
if param == keys.up then
-- Up
if nHistoryPos == nil then
if #_tHistory > 0 then
nHistoryPos = #_tHistory
end
elseif nHistoryPos > 1 then
nHistoryPos = nHistoryPos - 1
end
else
-- Down
if nHistoryPos == #_tHistory then
nHistoryPos = nil
elseif nHistoryPos ~= nil then
nHistoryPos = nHistoryPos + 1
end
end
if nHistoryPos then
sLine = _tHistory[nHistoryPos]
nPos, nScroll = #sLine, 0
else
sLine = ""
nPos, nScroll = 0, 0
end
uncomplete()
redraw()
end
elseif param == keys.back then
-- Backspace
if nPos > 0 then
clear()
sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1)
nPos = nPos - 1
if nScroll > 0 then nScroll = nScroll - 1 end
recomplete()
redraw()
end
elseif param == keys.home then
-- Home
if nPos > 0 then
clear()
nPos = 0
recomplete()
redraw()
end
elseif param == keys.delete then
-- Delete
if nPos < #sLine then
clear()
sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
recomplete()
redraw()
end
elseif param == keys["end"] then
-- End
if nPos < #sLine then
clear()
nPos = #sLine
recomplete()
redraw()
end
elseif param == keys.tab then
-- Tab (accept autocomplete)
acceptCompletion()
end
elseif sEvent == "mouse_down" or sEvent == "mouse_drag" and param == 1 then
local _, cy = term.getPos()
if param1 >= sx and param1 <= w and param2 == cy then
-- Ensure we don't scroll beyond the current line
nPos = math.min(math.max(nScroll + param1 - sx, 0), #sLine)
redraw()
end
elseif sEvent == "term_resize" then
-- Terminal resized
w = term.getSize()
redraw()
end
end
local _, cy = term.getPos()
term.setBlink(false)
term.setPos(w + 1, cy)
print()
return sLine
end
--[[function io.write(text)
text = tostring(text)
local lines = 0
local w, h = term.getSize()
local function inc_cy(cy)
lines = lines + 1
if cy > h - 1 then
term.scroll(1)
return cy
else
return cy + 1
end
end
while #text > 0 do
local nl = text:find("\n") or #text
local chunk = text:sub(1, nl)
text = text:sub(#chunk + 1)
local has_nl = chunk:sub(-1) == "\n"
if has_nl then chunk = chunk:sub(1, -2) end
local cx, cy = term.getPos()
while #chunk > 0 do
if cx > w then
term.setPos(1, inc_cy(cy))
cx, cy = term.getPos()
end
local to_write = chunk:sub(1, w - cx + 1)
term.write(to_write)
chunk = chunk:sub(#to_write + 1)
cx, cy = term.getPos()
end
if has_nl then
term.setPos(1, inc_cy(cy))
end
end
return lines
end
local empty = {}
function io.read(replace, history, complete, default)
expect(1, replace, "string", "nil")
expect(2, history, "table", "nil")
expect(3, complete, "function", "nil")
expect(4, default, "string", "nil")
if replace then replace = replace:sub(1, 1) end
local hist = history or {}
history = {}
for i = 1, #hist, 1 do
history[i] = hist[i]
end
local buffer = default or ""
local prev_buf = buffer
history[#history + 1] = buffer
local hist_pos = #history
local cursor_pos = 0
local stx, sty = term.getPos()
local w, h = term.getSize()
local dirty = false
local completions = {}
local comp_id = 0
local function clearCompletion()
if completions[comp_id] then
io.write((" "):rep(#completions[comp_id]))
end
end
local function full_redraw(force)
if force or dirty then
if complete and buffer ~= prev_buf then
completions = complete(buffer) or empty
comp_id = math.min(1, #completions)
end
prev_buf = buffer
term.setPos(stx, sty)
local text = buffer
if replace then text = replace:rep(#text) end
local ln = io.write(text)
if completions[comp_id] then
local oldfg = term.getForeground()
local oldbg = term.getBackground()
term.setForeground(colors.white)
term.setBackground(colors.gray)
ln = ln + write(completions[comp_id])
term.setForeground(oldfg)
term.setBackground(oldbg)
else
ln = ln + io.write(" ")
end
if sty + ln > h then
sty = sty - (sty + ln - h)
end
end
-- set cursor to the appropriate spot
local cx, cy = stx, sty
cx = cx + #buffer - cursor_pos -- + #(completions[comp_id] or "")
while cx > w do
cx = cx - w
cy = cy + 1
end
term.setPos(cx, cy)
end
term.setBlink(true)
while true do
full_redraw()
-- get input
local evt, par1, par2, mods = event.pull()
if evt == "char" then
dirty = true
clearCompletion()
if cursor_pos == 0 then
buffer = buffer .. par1
elseif cursor_pos == #buffer then
buffer = par1 .. buffer
else
buffer = buffer:sub(0, -cursor_pos - 1) .. par1 .. buffer:sub(-cursor_pos)
end
elseif evt == "key_down" then
if par1 == keys.back and #buffer > 0 then
dirty = true
if cursor_pos == 0 then
buffer = buffer:sub(1, -2)
clearCompletion()
elseif cursor_pos < #buffer then
buffer = buffer:sub(0, -cursor_pos - 2) .. buffer:sub(-cursor_pos)
end
elseif par1 == keys.delete and cursor_pos > 0 then
dirty = true
if cursor_pos == #buffer then
buffer = buffer:sub(2)
elseif cursor_pos == 1 then
buffer = buffer:sub(1, -2)
else
buffer = buffer:sub(0, -cursor_pos - 1) .. buffer:sub(-cursor_pos + 1)
end
cursor_pos = cursor_pos - 1
elseif par1 == keys.up then
if #completions > 1 then
dirty = true
clearCompletion()
if comp_id > 1 then
comp_id = comp_id - 1
else
comp_id = #completions
end
elseif hist_pos > 1 then
cursor_pos = 0
history[hist_pos] = buffer
hist_pos = hist_pos - 1
buffer = (" "):rep(#buffer)
full_redraw(true)
buffer = history[hist_pos]
dirty = true
end
elseif par1 == keys.down then
if #completions > 1 then
dirty = true
clearCompletion()
if comp_id < #completions then
comp_id = comp_id + 1
else
comp_id = 1
end
elseif hist_pos < #history then
cursor_pos = 0
history[hist_pos] = buffer
hist_pos = hist_pos + 1
buffer = (" "):rep(#buffer)
full_redraw(true)
buffer = history[hist_pos]
dirty = true
end
elseif par1 == keys.left then
if cursor_pos < #buffer then
clearCompletion()
cursor_pos = cursor_pos + 1
end
elseif par1 == keys.right then
if cursor_pos > 0 then
cursor_pos = cursor_pos - 1
elseif comp_id > 0 then
dirty = true
buffer = buffer .. completions[comp_id]
end
elseif par1 == keys.tab then
if comp_id > 0 then
dirty = true
buffer = buffer .. completions[comp_id]
end
elseif par1 == keys.home then
cursor_pos = #buffer
elseif par1 == keys["end"] then
cursor_pos = 0
elseif par1 == keys.enter then
clearCompletion()
print()
break
elseif mods & keys.mods.ctrl ~= 0 then
if par1 == keys.v then
dirty = true
clearCompletion()
local text = machine.getClipboard()
if text then
if cursor_pos == 0 then
buffer = buffer .. text
elseif cursor_pos == #buffer then
buffer = text .. buffer
else
buffer = buffer:sub(0, -cursor_pos - 1) .. text ..
buffer:sub(-cursor_pos + (#text - 1))
end
end
end
end
end
end
term.setBlink(false)
return buffer
end
]]
io.stderr = {}
function io.stderr.write(text)
local fg = term.getForeground()
term.setForeground(0xff0000)
io.write(text)
term.setForeground(fg)
end
function io.stderr.print(...)
local fg = term.getForeground()
term.setForeground(0xff0000)
print(...)
term.setForeground(fg)
end
return io

View file

@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View file

@ -0,0 +1,179 @@
local keys = {
none = 0,
back = 8,
tab = 9,
enter = 13,
pause = 19,
caps_lock = 20,
kana = 21,
kanji = 25,
escape = 27,
ime_convert = 28,
ime_no_convert = 29,
space = 32,
page_up = 33,
page_down = 34,
["end"] = 35,
home = 36,
left = 37,
up = 38,
right = 39,
down = 40,
select = 41,
print = 42,
execute = 43,
print_screen = 44,
insert = 45,
delete = 46,
help = 47,
zero = 48,
one = 49,
two = 50,
three = 51,
four = 52,
five = 53,
six = 54,
seven = 55,
eight = 56,
nine = 57,
a = 65,
b = 66,
c = 67,
d = 68,
e = 69,
f = 70,
g = 71,
h = 72,
i = 73,
j = 74,
k = 75,
l = 76,
m = 77,
n = 78,
o = 79,
p = 80,
q = 81,
r = 82,
s = 83,
t = 84,
u = 85,
v = 86,
w = 87,
x = 88,
y = 89,
z = 90,
left_windows = 91,
right_windows = 92,
apps = 93,
sleep = 95,
num_pad0 = 96,
num_pad1 = 97,
num_pad2 = 98,
num_pad3 = 99,
num_pad4 = 100,
num_pad5 = 101,
num_pad6 = 102,
num_pad7 = 103,
num_pad8 = 104,
num_pad9 = 105,
multiply = 106,
add = 107,
separator = 108,
subtract = 109,
decimal = 110,
divide = 111,
f1 = 112,
f2 = 113,
f3 = 114,
f4 = 115,
f5 = 116,
f6 = 117,
f7 = 118,
f8 = 119,
f9 = 120,
f10 = 121,
f11 = 122,
f12 = 123,
f13 = 124,
f14 = 125,
f15 = 126,
f16 = 127,
f17 = 128,
f18 = 129,
f19 = 130,
f20 = 131,
f21 = 132,
f22 = 133,
f23 = 134,
f24 = 135,
num_lock = 144,
scroll = 145,
left_shift = 160,
right_shift = 161,
left_control = 162,
right_control = 163,
left_alt = 164,
right_alt = 165,
browser_back = 166,
browser_forward = 167,
browser_refresh = 168,
browser_stop = 169,
browser_search = 170,
browser_favorites = 171,
browser_home = 172,
volume_mute = 173,
volume_down = 174,
volume_up = 175,
media_next_track = 176,
media_previous_track = 177,
media_stop = 178,
media_play_pause = 179,
launch_mail = 180,
select_media = 181,
launch_application1 = 182,
launch_application2 = 183,
semicolon = 186,
plus = 187,
comma = 188,
minus = 189,
period = 190,
question = 191,
tilde = 192,
chat_pad_green = 202,
chat_pad_orange = 203,
open_brackets = 219,
pipe = 220,
close_brackets = 221,
quotes = 222,
oem8 = 223,
backslash = 226,
process_key = 229,
copy = 242,
auto = 243,
enl_w = 244,
attn = 246,
crsel = 247,
exsel = 248,
erase_eof = 249,
play = 250,
zoom = 251,
pa1 = 253,
clear = 254,
}
keys.mods = {
none = 0,
left_shift = 1,
right_shift = 2,
left_alt = 4,
right_alt = 8,
left_control = 16,
right_control = 32,
}
keys.mods.shift = keys.mods.left_shift | keys.mods.right_shift
keys.mods.alt = keys.mods.left_alt | keys.mods.right_alt
keys.mods.control = keys.mods.left_control | keys.mods.right_control
keys.mods.ctrl = keys.mods.control
return keys

View file

@ -0,0 +1,3 @@
local x = math.random()
return x

View file

@ -0,0 +1,61 @@
local expect = require("expect")
local parallel = {}
local function contains(array, value)
for k, v in pairs(array) do
if v == value then
return true
end
end
return false
end
local function run(threads, exitOnAny)
local alive = #threads
local filters = {}
local ev = {}
while true do
for i, thread in pairs(threads) do
if not filters[i] or #filters[i] == 0 or contains(filters[i], ev[1]) or ev[1] == "interrupt" then
local pars = table.pack(coroutine.resume(thread, table.unpack(ev)))
if pars[1] then
filters[i] = table.pack(table.unpack(pars, 2))
else
error(pars[2], 0)
end
end
if coroutine.status(thread) == "dead" then
alive = alive - 1
if exitOnAny or alive <= 0 then
return
end
end
end
ev = table.pack(coroutine.yield())
end
end
function parallel.waitForAll(...)
local threads = {}
for k, v in ipairs({ ... }) do
expect(k, v, "function")
table.insert(threads, coroutine.create(v))
end
return run(threads, false)
end
function parallel.waitForAny(...)
local threads = {}
for k, v in ipairs({ ... }) do
expect(k, v, "function")
table.insert(threads, coroutine.create(v))
end
return run(threads, true)
end
return parallel

View file

@ -0,0 +1,182 @@
local expect = require("expect").expect
local tableutils = require("tableutils")
local event = require("event")
local scheduler = {}
local function contains(array, value)
for k, v in pairs(array) do
if v == value then
return true
end
end
return false
end
local tasks = {}
local processes = 0
local Task = {}
local TaskMeta = {
__index = Task,
__name = "OS_TASK",
__tostring = function(self)
return string.format("OS_TASK[%s]: %d", self.source or "", self.pid or 0)
end,
}
local function newTask()
local task = {}
return setmetatable(task, TaskMeta)
end
function Task:queue(eventName, ...)
expect(1, eventName, "string")
event.push("scheduler", self.pid, eventName, ...)
end
local function findParent()
local i = 3
while true do
local info = debug.getinfo(i)
if not info then
break
end
for pid, task in pairs(tasks) do
if task.uuid == tostring(info.func) then
return task
end
end
i = i + 1
end
return nil
end
local function cascadeKill(pid, err)
local task = tasks[pid]
if not task then
return
end
for i, cpid in ipairs(task.children) do
cascadeKill(cpid, err)
end
if task.parent then
local parent = tasks[task.parent]
if parent then
local index = tableutils.find(parent.children, task.pid)
table.remove(parent.children, index)
parent:queue("scheduler_task_end", task, err == nil, err)
end
else
if err then
error(err, 0)
end
end
if task then
task.killed = true
coroutine.close(task.thread)
tasks[pid] = nil
processes = processes - 1
end
end
local function resumeTask(task, yieldPars)
local pars = table.pack(coroutine.resume(task.thread, table.unpack(yieldPars)))
if pars[1] then
task.filters = table.pack(table.unpack(pars, 2))
return coroutine.status(task.thread) ~= "dead"
else
cascadeKill(task.pid, pars[2])
return false
end
end
function scheduler.spawn(func, options)
expect(1, func, "function")
expect(2, options, "nil", "table")
options = options or {}
options.args = options.args or {}
local source = debug.getinfo(2)
local task = newTask()
local pid = #tasks + 1
task.pid = pid
task.options = options
task.source = source.source
task.uuid = tostring(func)
task.thread = coroutine.create(func)
local parent = findParent()
if parent then
task.parent = parent.pid
table.insert(parent.children, pid)
end
task.filters = {}
task.children = {}
task.eventQueue = {}
task.skip = true
tasks[pid] = task
processes = processes + 1
return task, resumeTask(task, task.options.args)
end
function scheduler.kill(pid)
expect(1, pid, "number")
cascadeKill(pid)
end
function scheduler.ipc(pid, ...)
expect(1, pid, "number")
if not tasks[pid] then
error("process by pid " .. pid .. " does not exist.", 2)
end
local sender = findParent()
tasks[pid]:queue("ipc_message", sender, ...)
end
local running = false
function scheduler.init()
if running then
error("scheduler already running", 2)
end
running = true
local ev = { n = 0 }
while processes > 0 do
for pid, task in pairs(tasks) do
local yieldPars = ev
if ev[1] == "scheduler" and ev[2] == pid then
yieldPars = table.pack(table.unpack(ev, 3))
end
if yieldPars[1] ~= "scheduler" and not task.filters or #task.filters == 0 or contains(task.filters, yieldPars[1]) or yieldPars[1] == "interrupt" then
if task.skip then
task.skip = false
else
resumeTask(task, yieldPars)
end
end
if coroutine.status(task.thread) == "dead" then
cascadeKill(pid)
end
end
if processes <= 0 then
break
end
ev = table.pack(coroutine.yield())
end
running = false
end
return scheduler

View file

@ -0,0 +1,112 @@
local expect = require("expect").expect
local fs = require("fs")
local nativePackage = package
local function copyTable(source, target)
target = target or {}
for k, v in pairs(source) do
target[k] = v
end
return target
end
local hostPackages = copyTable(nativePackage._host)
local function createPreloadSearcher(envPackage)
return function(name)
if not envPackage.preload[name] then
return string.format("no field package.preload['%s']", name)
end
return envPackage.preload[name], ":preload:"
end
end
local function createLoaderSearcher(envPackage)
return function(name)
local path, err = envPackage.searchpath(name, envPackage.path)
if not path then
return err
end
local func, err = loadfile(path)
if not func then
return string.format("error loading module '%s' from file '%s':\t%s", name, path, err)
end
return func, path
end
end
local function createEnvironment(filePath)
local envPackage = {
cpath = nativePackage.cpath,
searchpath = nativePackage.searchpath,
config = nativePackage.config,
searchers = {},
loaded = {},
preload = {},
}
local dirName = fs.getDir(filePath)
--envPackage.path = string.format("%s/?.lua;%s/?/init.lua;", dirName, dirName) .. nativePackage.path
envPackage.path = nativePackage.path
envPackage.searchers[1] = createPreloadSearcher(envPackage)
envPackage.searchers[2] = createLoaderSearcher(envPackage)
local function envRequire(modname)
expect(1, modname, "string", "number")
modname = tostring(modname)
if envPackage.loaded[modname] then
return envPackage.loaded[modname]
end
local errorOutput = ""
local libFunction, libPath
for i = 1, #envPackage.searchers do
local par, path = envPackage.searchers[i](modname)
if type(par) == "function" then
libFunction, libPath = par, path
break
else
errorOutput = errorOutput .. "\n\t" .. par
end
end
if not libFunction then
error(string.format("module '%s' not found:%s", modname, errorOutput), 2)
end
local ok, par = pcall(libFunction)
if not ok then
error(par, 0)
end
if par == nil then
envPackage.loaded[modname] = true
return true
end
envPackage.loaded[modname] = par
return par, libPath
end
copyTable(hostPackages, envPackage.loaded)
envPackage.loaded.package = envPackage
local env_G = copyTable(envPackage.loaded._G or _G)
envPackage.loaded._G = env_G
env_G._G = env_G
envPackage.loaded._G.package = envPackage
envPackage.loaded._G.require = envRequire
return envPackage, envRequire
end
return createEnvironment

View file

@ -0,0 +1,87 @@
local expect = require("expect").expect
local tableutils = {}
local function serialize(data, circular)
expect(1, data, "string", "table", "number", "boolean", "nil")
if type(data) == "table" then
if not circular then
circular = {}
end
local output = "{"
for k, v in pairs(data) do
if type(v) == "table" then
local name = tostring(v)
if circular[name] then
error("circular reference in table", 2)
end
circular[name] = true
end
output = output .. string.format("[%q] = %s,", k, serialize(v, circular))
end
output = output .. "}"
return output
else
return string.format("%q", data)
end
end
function tableutils.serialize(data)
expect(1, data, "string", "table", "number", "boolean", "nil")
return serialize(data)
end
function tableutils.deserialize(data)
local func, err = load("return " .. data, "=tableutils", "t", {})
if not func then
error(err, 2)
end
return func()
end
local function prettyvalue(value)
if type(value) == "table" or type(value) == "function" or type(value) == "thread" or type(value) == "userdata" or type(value) == "number" then
return tostring(value)
else
return string.format("%q", value)
end
end
function tableutils.pretty(data)
if type(data) == "table" then
local output = "{"
local index = 0
for k, v in pairs(data) do
local value = prettyvalue(v)
if type(k) == "number" and k - 1 == index then
index = index + 1
output = output .. string.format("\n %s,", value)
elseif type(k) == "string" and k:match("^[%a_][%w_]*$") then
output = output .. string.format("\n %s = %s,", k, value)
else
output = output .. string.format("\n [%s] = %s,", prettyvalue(k), value)
end
end
if output == "{" then
return "{}"
end
output = output .. "\n}"
return output
else
return prettyvalue(data)
end
end
function tableutils.find(tbl, element)
for i = 1, #tbl do
if tbl[i] == element then
return i
end
end
return nil
end
return tableutils

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
Welcome to CapyOS!
Run "programs" to get a list of available programs.

View file

@ -0,0 +1,12 @@
CapyOS and Capy64 are licensed under the Apache 2.0 Public License.
https://capy64.alexdevs.me/
https://github.com/Ale32bit/Capy64
Some CapyOS components include code from the CC Recrafted project.
https://github.com/Ocawesome101/recrafted
Some CapyOS components may include code from CC: Tweaked.
https://github.com/CC-Tweaked/CC-Tweaked
json.lua by rxi is licensed under the MIT License.
https://github.com/rxi/json.lua

View file

@ -0,0 +1,3 @@
You can get started with Lua by following this manual: https://www.lua.org/manual/
Learn more about Capy64 by following the documentation: https://capy64.alexdevs.me/
Found a bug or would like to suggest a feature? https://github.com/Ale32bit/Capy64/issues

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,5 @@
If you are looking for your data directory look in:
Windows: %APPDATA%\Capy64\data
Mac: $HOME/.local/share/Capy64/data
Linux: $HOME/.local/share/Capy64/data

View file

@ -1,4 +1,4 @@
-- This file is part of Capy64 - https://github.com/Capy64/Capy64
-- This file is part of Capy64 - https://github.com/Capy64/Capy64
-- Copyright 2023 Alessandro "AlexDevs" Proto
--
-- Licensed under the Apache License, Version 2.0 (the "License").
@ -19,22 +19,22 @@ local gpu = require("gpu")
local fs = require("fs")
local machine = require("machine")
local audio = require("audio")
local http = require("http")
local event = require("event")
local INDEX_URL = "https://raw.github.com/Ale32bit/CapyOS/deploy/index.json"
local JSON_URL = "https://raw.github.com/Ale32bit/CapyOS/main/lib/json.lua"
local bootSleep = 2000
local bootSleep = 2
local bg = 0x0
local fg = 0xffffff
local setupbg = 0x0608a6
local setupfg = 0xffffff
local accent = 0xffea00
term.setForeground(fg)
term.setBackground(bg)
term.clear()
term.setSize(53, 20)
if term.isResizable() then
term.setSize(53, 20)
end
local w, h = term.getSize()
@ -56,13 +56,13 @@ local function writeCenter(text)
end
local function drawVendorImage()
if not fs.exists("/boot/vendor.bmp") then
if not fs.exists("/sys/vendor.bmp") then
return
end
local w, h = gpu.getSize()
local ok, err = pcall(function()
local task<close> = gpu.loadImageAsync("/boot/vendor.bmp")
local task<close> = gpu.loadImageAsync("/sys/vendor.bmp")
local buffer<close> = task:await()
local x, y =
@ -95,62 +95,65 @@ local function printError( text )
term.setForeground(0xffffff)
end
local function hget(url, headers)
local response, err = http.requestAsync(url, nul, headers, {binary = true}):await()
if not response then
return nil, err
end
local content = response.content:read("a")
response.content:close()
return content, response
end
local function promptKey()
print("Press any key to continue")
event.pull("key_down")
end
local function installOS()
term.clear()
term.setPos(1, 1)
print("Installing CapyOS...")
local jsonLib, par = hget(JSON_URL)
if not jsonLib then
printError(par)
promptKey()
return
local function alert(...)
local args = {...}
table.insert(args, "[ OK ]")
local lines = {}
local width = 0
local padding = 1
for k, v in ipairs(args) do
lines[k] = tostring(v)
width = math.max(width, #tostring(v))
end
local json = load(jsonLib)()
local indexData, par = hget(INDEX_URL)
if not indexData then
printError(par)
promptKey()
return
lines[#lines] = nil
local okPad = string.rep(" ", (width - #args[#args]) // 2)
local okLine = string.format("%s%s", okPad, args[#args])
local w, h = term.getSize()
local cx, cy = w//2, h//2
local dx, dy = cx - (width + padding * 2) // 2, cy - #lines//2 - 1
local pad = string.rep(" ", padding)
local emptyLine = string.format("\u{258C}%s%s%s\u{2590}", pad, string.rep(" ", width), pad)
term.setPos(dx, dy)
print("\u{259B}" .. string.rep("\u{2580}", width + padding * 2) .. "\u{259C}")
for k, v in ipairs(lines) do
term.setPos(dx, dy + k)
local space = string.rep(" ", width - #v)
print(string.format("\u{258C}%s%s%s%s\u{2590}", pad, v, space, pad))
end
local index = json.decode(indexData)
term.setPos(dx, dy + #lines + 1)
print(emptyLine)
term.setPos(dx, dy + #lines + 2)
local space = string.rep(" ", width - #okLine)
term.write(string.format("\u{258C}%s", pad))
term.setForeground(accent)
term.write(okLine)
term.setForeground(setupfg)
print(string.format("%s%s\u{2590}", space, pad))
term.setPos(dx, dy + #lines + 3)
print("\u{2599}" .. string.rep("\u{2584}", width + padding * 2) .. "\u{259F}")
for i, v in ipairs(index) do
local dirname = fs.getDir(v.path)
if not fs.exists(dirname) then
fs.makeDir(dirname)
end
print("Downloading " .. v.path)
local fileContent = hget(v.raw_url)
local f = fs.open(v.path, "w")
f:write(fileContent)
f:close()
print("Written to " .. v.path)
local _, key, keyname
repeat
_, key, keyname = event.pull("key_down")
until keyname == "enter" or keyname == "escape"
end
local function installDefaultOS()
if fs.exists("/sys") then
fs.delete("/sys", true)
end
flagInstalled()
print("CapyOS installed!")
promptKey()
installOS()
alert("Default OS installed!")
end
term.setBlink(false)
@ -163,8 +166,9 @@ local function setupScreen()
},
{
"Install default OS",
installOS,
installDefaultOS,
},
{},
{
"Exit setup",
exit,
@ -176,26 +180,40 @@ local function setupScreen()
}
local selection = 1
local function redraw()
term.setForeground(fg)
term.setBackground(bg)
local function redraw(noDrawSel)
local w, h = term.getSize()
term.setForeground(setupfg)
term.setBackground(setupbg)
term.clear()
term.setPos(1,2)
writeCenter("Capy64 Setup")
term.setPos(1,3)
term.setForeground(accent)
for k, v in ipairs(options) do
local _, y = term.getPos()
term.setPos(1, y + 1)
term.clearLine()
if selection == k then
writeCenter("[ " .. v[1] .. " ]")
else
writeCenter(v[1])
if #v > 0 then
if selection == k and not noDrawSel then
writeCenter("[ " .. v[1] .. " ]")
else
writeCenter(v[1])
end
end
end
term.setForeground(setupfg)
term.setPos(1, h - 2)
term.write("\u{2190}\u{2191}\u{2192}\u{2193}: Move")
term.setPos(1, h - 1)
term.write("Escape: Exit")
term.setPos(1, h)
term.write("Enter: Select")
end
while true do
@ -203,9 +221,16 @@ local function setupScreen()
local ev = { coroutine.yield("key_down") }
if ev[3] == "up" then
selection = selection - 1
if options[selection] and #options[selection] == 0 then
selection = selection - 1
end
elseif ev[3] == "down" then
selection = selection + 1
if options[selection] and #options[selection] == 0 then
selection = selection + 1
end
elseif ev[3] == "enter" then
redraw(true)
options[selection][2]()
elseif ev[3] == "escape" then
exit()
@ -226,7 +251,7 @@ local function bootScreen()
term.setPos(1,2)
writeCenter("Capy64")
term.setPos(1,4)
writeCenter("Powered by Capybaras")
writeCenter("(c) 2023 AlexDevs")
term.setPos(1, h - 1)
writeCenter("Press F2 to open setup")
@ -245,10 +270,6 @@ end
audio.beep(1000, 0.2, 0.2, "square")
if shouldInstallOS() then
installOS()
end
bootScreen()
term.clear()

View file

@ -0,0 +1,51 @@
-- This file is part of Capy64 - https://github.com/Capy64/Capy64
-- Copyright 2023 Alessandro "AlexDevs" Proto
--
-- Licensed under the Apache License, Version 2.0 (the "License").
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
local event = require("event")
local coroutine = coroutine
-- Declare event functions
function event.pull(...)
local ev = table.pack(coroutine.yield(...))
if ev[1] == "interrupt" then
error("Interrupted", 2)
end
return table.unpack(ev)
end
function event.pullRaw(...)
return coroutine.yield(...)
end
-- Set task awaiter
local function awaiter(task)
local status = task:getStatus()
local uuid = task:getID()
if status == "running" then
local _, taskId, result, err
repeat
_, taskId, result, err = event.pull("task_finish")
until taskId == uuid
return result, err
elseif status == "succeeded" then
return task:getResult(), nil
elseif status == "failed" then
return nil, task:getError()
end
end
-- Second argument freezes the awaiter function, so it cannot be modified
event.setAwaiter(awaiter, true)

View file

@ -1,5 +1,6 @@
{
"EngineMode": 0,
"SafeMode": false,
"Window": {
"Scale": 2
},

View file

@ -21,14 +21,15 @@ using Capy64.Integrations;
using Capy64.PluginManager;
using Capy64.Runtime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using static Capy64.Utils;
@ -40,10 +41,9 @@ public enum EngineMode
Free
}
public class Capy64 : Game, IGame
public class Capy64 : Game
{
public const string Version = "0.0.10-alpha";
public const string Version = "1.1.2-beta";
public static class DefaultParameters
{
@ -51,12 +51,14 @@ public class Capy64 : Game, IGame
public const int Height = 240;
public const float Scale = 2f;
public const float BorderMultiplier = 1.5f;
public readonly static EngineMode EngineMode = EngineMode.Classic;
public static readonly EngineMode EngineMode = EngineMode.Classic;
public const int ClassicTickrate = 20;
public const int ClassicTickrate = 30;
public const int FreeTickrate = 60;
}
public static readonly string AssemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
public static readonly string AssetsPath = Path.Combine(AssemblyPath, "Assets");
public static string AppDataPath
{
@ -64,12 +66,15 @@ public class Capy64 : Game, IGame
{
string baseDir =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create) :
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData,
Environment.SpecialFolderOption.Create) :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ?
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create) :
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create) :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create) :
"./";
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create) :
"./";
return Path.Combine(baseDir, "Capy64");
}
@ -88,8 +93,11 @@ public class Capy64 : Game, IGame
public LuaState LuaRuntime { get; set; }
public Eventing.EventEmitter EventEmitter { get; private set; }
public DiscordIntegration Discord { get; set; }
public int TickRate => tickrate;
public IConfiguration Configuration { get; private set; }
public Color BorderColor { get; set; } = Color.Black;
public Borders Borders = new()
{
Top = 0,
@ -97,22 +105,23 @@ public class Capy64 : Game, IGame
Left = 0,
Right = 0,
};
public SpriteBatch SpriteBatch;
private readonly InputManager _inputManager;
private RenderTarget2D renderTarget;
private readonly GraphicsDeviceManager _graphics;
private IServiceProvider _serviceProvider;
private ulong _totalTicks = 0;
private ulong tickrate = 0;
private int tickrate = 0;
private int everyTick => 60 / tickrate;
public Capy64()
{
Instance = this;
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
//Content.RootDirectory = "Content";
IsMouseVisible = true;
EventEmitter = new();
@ -121,11 +130,6 @@ public class Capy64 : Game, IGame
Drawing = new();
}
public void ConfigureServices(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void SetEngineMode(EngineMode mode)
{
switch (mode)
@ -144,6 +148,9 @@ public class Capy64 : Game, IGame
Window.AllowUserResizing = true;
break;
}
EngineMode = mode;
UpdateSize(true);
}
@ -173,6 +180,12 @@ public class Capy64 : Game, IGame
private void OnWindowSizeChange(object sender, EventArgs e)
{
if (EngineMode == EngineMode.Classic)
{
UpdateSize(true);
return;
}
var bounds = Window.ClientBounds;
Width = (int)(bounds.Width / Scale);
@ -196,7 +209,6 @@ public class Capy64 : Game, IGame
ResetBorder();
UpdateSize();
}
}
private void ResetBorder()
@ -213,11 +225,26 @@ public class Capy64 : Game, IGame
protected override void Initialize()
{
var configuration = _serviceProvider.GetService<IConfiguration>();
var configBuilder = new ConfigurationBuilder();
var settingsPath = Path.Combine(AppDataPath, "settings.json");
if (!Directory.Exists(AppDataPath))
{
Directory.CreateDirectory(AppDataPath);
}
if (!File.Exists(settingsPath))
{
File.Copy(Path.Combine(AssetsPath, "default.json"), settingsPath);
}
configBuilder.AddJsonFile(Path.Combine(AssetsPath, "default.json"), false);
configBuilder.AddJsonFile(settingsPath, false);
Configuration = configBuilder.Build();
Window.Title = "Capy64 " + Version;
Scale = configuration.GetValue("Window:Scale", DefaultParameters.Scale);
Scale = Configuration.GetValue("Window:Scale", DefaultParameters.Scale);
ResetBorder();
UpdateSize();
@ -227,12 +254,14 @@ public class Capy64 : Game, IGame
InactiveSleepTime = new TimeSpan(0);
SetEngineMode(configuration.GetValue<EngineMode>("EngineMode", DefaultParameters.EngineMode));
SetEngineMode(Configuration.GetValue<EngineMode>("EngineMode", DefaultParameters.EngineMode));
Audio = new Audio();
NativePlugins = GetNativePlugins();
Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider);
var safeMode = Configuration.GetValue("SafeMode", false);
if (!safeMode)
Plugins = PluginLoader.LoadAllPlugins(Path.Combine(AppDataPath, "plugins"));
EventEmitter.RaiseInit();
@ -250,9 +279,10 @@ public class Capy64 : Game, IGame
foreach (var type in types)
{
var instance = (IComponent)ActivatorUtilities.CreateInstance(_serviceProvider, type)!;
var instance = (IComponent)Activator.CreateInstance(type, this);
plugins.Add(instance);
}
return plugins;
}
@ -273,7 +303,7 @@ public class Capy64 : Game, IGame
{
GameTime = gameTime,
TotalTicks = _totalTicks,
IsActiveTick = _totalTicks % (60 / tickrate) == 0,
IsActiveTick = (int)_totalTicks % everyTick == 0,
});
Drawing.End();
@ -289,9 +319,11 @@ public class Capy64 : Game, IGame
GraphicsDevice.Clear(BorderColor);
SpriteBatch.DrawRectangle(renderTarget.Bounds.Location.ToVector2() + new Vector2(Borders.Left, Borders.Top),
new Size2(renderTarget.Bounds.Width * Scale, renderTarget.Bounds.Height * Scale), Color.Black, Math.Min(renderTarget.Bounds.Width, renderTarget.Bounds.Height), 0);
new Size2(renderTarget.Bounds.Width * Scale, renderTarget.Bounds.Height * Scale), Color.Black,
Math.Min(renderTarget.Bounds.Width, renderTarget.Bounds.Height), 0);
SpriteBatch.Draw(renderTarget, new(Borders.Left, Borders.Top), null, Color.White, 0f, Vector2.Zero, Scale, SpriteEffects.None, 0);
SpriteBatch.Draw(renderTarget, new(Borders.Left, Borders.Top), null, Color.White, 0f, Vector2.Zero, Scale,
SpriteEffects.None, 0);
EventEmitter.RaiseOverlay(new()
{

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
@ -26,9 +26,9 @@
</ItemGroup>
<ItemGroup>
<None Update="Assets\Lua/**">
<Content Include="Assets\Lua\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</Content>
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE">
@ -37,26 +37,22 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.2.8" />
<PackageReference Include="KeraLua" Version="1.3.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.3.6" />
<PackageReference Include="KeraLua" Version="1.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="MonoGame.Extended.Graphics" Version="3.8.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
<PackageReference Include="System.ComponentModel.Composition" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\bios.lua">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<EditorConfigFiles Remove="C:\Users\Alex\source\repos\Capy64\Capy64\Capy64\.editorconfig" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" />
</Target>
<ItemGroup>
<EditorConfigFiles Remove="C:\Users\Alex\source\repos\Capy64\Capy64\Capy64\.editorconfig" />
<Folder Include="Assets\Lua\" />
</ItemGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">

View file

@ -1,15 +0,0 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:DesktopGL
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#

View file

@ -29,12 +29,12 @@ public class Audio : IDisposable
Noise
}
public const int SampleRate = 16000;
public const int SampleRate = 24000;
public const int HQSampleRate = 48000;
public const AudioChannels AudioChannel = AudioChannels.Mono;
public const int ChannelsCount = 8;
public readonly DynamicSoundEffectInstance[] Channels = new DynamicSoundEffectInstance[ChannelsCount];
private bool[] freeChannels = new bool[ChannelsCount];
private readonly bool[] freeChannels = new bool[ChannelsCount];
public readonly DynamicSoundEffectInstance HQChannel = new(HQSampleRate, AudioChannel);
@ -126,6 +126,7 @@ public class Audio : IDisposable
public TimeSpan SubmitHQ(byte[] buffer)
{
HQChannel.SubmitBuffer(buffer);
return HQChannel.GetSampleDuration(buffer.Length);
}

View file

@ -31,7 +31,7 @@ public class Drawing : IDisposable
private Texture2D _whitePixel;
private RenderTarget2D _canvas;
private bool _isDrawing;
private HashSet<Texture2D> _disposeTextures = new();
private readonly HashSet<Texture2D> _disposeTextures = new();
public RenderTarget2D Canvas
{
get => _canvas;
@ -54,7 +54,7 @@ public class Drawing : IDisposable
public Drawing()
{
_fontSystem = new FontSystem();
_fontSystem.AddFont(File.ReadAllBytes(@"Assets/font.ttf"));
_fontSystem.AddFont(File.ReadAllBytes(Path.Combine(Capy64.AssetsPath, "font.ttf")));
}
public void Begin()
@ -201,7 +201,7 @@ public class Drawing : IDisposable
_spriteBatch.Draw(_whitePixel, position3, null, color, rotation, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
}
public void DrawBuffer(uint[] buffer, Rectangle rect, Rectangle? source = null, Color? color = null, float rotation = 0f, Vector2? origin = null, float scale = 1f, SpriteEffects spriteEffects = 0)
public void DrawBuffer(uint[] buffer, Rectangle rect, Rectangle? source = null, Color? color = null, float rotation = 0f, Vector2? origin = null, Vector2? scale = null, SpriteEffects spriteEffects = 0)
{
var texture = new Texture2D(_graphicsDevice, rect.Width, rect.Height, false, SurfaceFormat.Color);
texture.SetData(buffer);
@ -213,7 +213,7 @@ public class Drawing : IDisposable
color ?? Color.White, // Color
rotation, // Rotation
origin ?? Vector2.Zero, // Origin
scale, // Scale
scale ?? Vector2.One, // Scale
spriteEffects, // Flip effects
0f // layer depth
);

View file

@ -52,7 +52,7 @@ public class InputManager
Ctrl = LCtrl | RCtrl,
}
private static Keys[] IgnoredTextInputKeys =
private static readonly Keys[] IgnoredTextInputKeys =
{
Keys.Enter,
Keys.Back,
@ -74,7 +74,7 @@ public class InputManager
};
public Texture2D Texture { get; set; }
public float WindowScale => Capy64.Instance.Scale;
public static float WindowScale => Capy64.Instance.Scale;
public const int MouseScrollDelta = 120;
private Point mousePosition;
@ -82,7 +82,7 @@ public class InputManager
private int hMouseScroll;
private Modifiers keyboardMods = 0;
private HashSet<Keys> pressedKeys = new();
private readonly HashSet<Keys> pressedKeys = new();
private readonly Game _game;
private readonly EventEmitter _eventEmitter;

View file

@ -105,7 +105,7 @@ public partial class SDL2
}
char* chars = stackalloc char[len];
int strLen = System.Text.Encoding.UTF8.GetChars((byte*)s, len, chars, len);
string result = new string(chars, 0, strLen);
string result = new(chars, 0, strLen);
#endif
/* Some SDL functions will malloc, we have to free! */

View file

@ -1,50 +0,0 @@
// This file is part of Capy64 - https://github.com/Ale32bit/Capy64
// Copyright 2023 Alessandro "AlexDevs" Proto
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64.API;
using Capy64.Core;
using Capy64.Integrations;
using Capy64.Runtime;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Capy64;
public interface IGame
{
Capy64 Game { get; }
EngineMode EngineMode { get; }
IList<IComponent> NativePlugins { get; }
IList<IComponent> Plugins { get; }
GameWindow Window { get; }
Drawing Drawing { get; }
Audio Audio { get; }
LuaState LuaRuntime { get; set; }
Eventing.EventEmitter EventEmitter { get; }
void ConfigureServices(IServiceProvider serviceProvider);
int Width { get; set; }
int Height { get; set; }
float Scale { get; set; }
void UpdateSize(bool resize = true);
event EventHandler<EventArgs> Exiting;
void Run();
void Exit();
// Integrations
DiscordIntegration Discord { get; }
}

View file

@ -25,29 +25,35 @@ namespace Capy64.Integrations;
public class DiscordIntegration : IComponent
{
public DiscordRpcClient Client { get; private set; }
public readonly bool Enabled;
private readonly IConfiguration _configuration;
public DiscordIntegration(IConfiguration configuration)
public DiscordIntegration(Capy64 game)
{
_configuration = configuration;
_configuration = game.Configuration;
var discordConfig = _configuration.GetSection("Integrations:Discord");
Client = new(discordConfig["ApplicationId"]);
Enabled = discordConfig.GetValue("Enable", false);
Client.Logger = new ConsoleLogger() { Level = DiscordRPC.Logging.LogLevel.Warning };
Client = new(discordConfig["ApplicationId"])
{
Logger = new ConsoleLogger() { Level = LogLevel.Warning }
};
Client.OnReady += OnReady;
Capy64.Instance.Discord = this;
if (discordConfig.GetValue("Enable", false))
{
if (Enabled)
Client.Initialize();
}
}
#nullable enable
public void SetPresence(string details, string? state = null)
{
if (!Enabled)
return;
Client.SetPresence(new RichPresence()
{
Details = details,

View file

@ -21,7 +21,7 @@ namespace Capy64.PluginManager
{
class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{

View file

@ -14,7 +14,6 @@
// limitations under the License.
using Capy64.API;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
@ -33,7 +32,7 @@ internal class PluginLoader
return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(path)));
}
public static List<IComponent> LoadAllPlugins(string pluginsPath, IServiceProvider provider)
public static List<IComponent> LoadAllPlugins(string pluginsPath)
{
if (!Directory.Exists(pluginsPath))
Directory.CreateDirectory(pluginsPath);
@ -47,7 +46,7 @@ internal class PluginLoader
{
if (typeof(IComponent).IsAssignableFrom(type))
{
IComponent result = ActivatorUtilities.CreateInstance(provider, type) as IComponent;
IComponent result = Activator.CreateInstance(type, Capy64.Instance) as IComponent;
plugins.Add(result);
}
}

View file

@ -13,34 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.IO;
using var game = new Capy64.Capy64();
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, c) =>
{
var settingsPath = Path.Combine(Capy64.Capy64.AppDataPath, "settings.json");
if (!Directory.Exists(Capy64.Capy64.AppDataPath))
{
Directory.CreateDirectory(Capy64.Capy64.AppDataPath);
}
if (!File.Exists(settingsPath))
{
File.Copy("Assets/default.json", settingsPath);
}
c.AddJsonFile("Assets/default.json", false);
c.AddJsonFile(settingsPath, false);
})
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IGame>(game);
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
game.Run();

View file

@ -18,4 +18,6 @@ namespace Capy64.Runtime;
public class Constants
{
public const int MULTRET = -1;
public const int MINSTACK = 20;
public const int MAXLENNUM = 200;
}

View file

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64.Core;
using Capy64.Eventing.Events;
using Microsoft.Xna.Framework.Input;
using System;
@ -24,8 +23,8 @@ namespace Capy64.Runtime;
internal class EventEmitter
{
private Eventing.EventEmitter _eventEmitter;
private LuaState _runtime;
private readonly Eventing.EventEmitter _eventEmitter;
private readonly LuaState _runtime;
private const int rebootDelay = 30;
private int heldReboot = 0;

View file

@ -97,17 +97,4 @@ public static class Utils
}
return 1;
}
[Obsolete("This method does not work as intended and requires more research")]
public static void PushManagedObject<T>(this Lua L, T obj)
{
var type = obj.GetType();
var members = type.GetMembers().Where(m => m.MemberType == MemberTypes.Method);
L.CreateTable(0, members.Count());
foreach (var m in members)
{
L.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L }));
L.SetField(-2, m.Name);
}
}
}
}

View file

@ -21,18 +21,18 @@ using static Capy64.Core.Audio;
namespace Capy64.Runtime.Libraries;
public class Audio : IComponent
public class AudioLib : IComponent
{
private const int queueLimit = 8;
private static IGame _game;
public Audio(IGame game)
private static Capy64 _game;
public AudioLib(Capy64 game)
{
_game = game;
_game.EventEmitter.OnClose += OnClose;
}
private static LuaRegister[] AudioLib = new LuaRegister[]
private static readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
@ -85,10 +85,18 @@ public class Audio : IComponent
private static int OpenLib(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(AudioLib);
L.NewLib(Library);
return 1;
}
// Convert 8bit PCM to 16bit PCM
private static void PCMTo16bit(Span<byte> buffer, int i, byte value)
{
var value16bit = value / sbyte.MaxValue * short.MaxValue;
buffer[2 * i - 2] = (byte)(value16bit & 0xff);
buffer[2 * i - 1] = (byte)(value16bit >> 8);
}
private static int L_Play(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -97,18 +105,24 @@ public class Audio : IComponent
if (L.IsString(1))
{
buffer = L.CheckBuffer(1);
var inputBuffer = L.CheckBuffer(1);
buffer = new byte[inputBuffer.Length * 2];
var span = new Span<byte>(buffer);
for (int i = 1; i < inputBuffer.Length; i++)
{
PCMTo16bit(span, i, inputBuffer[i]);
}
}
else
{
L.CheckType(1, LuaType.Table);
var len = L.RawLen(1);
buffer = new byte[len];
buffer = new byte[len * 2];
var span = new Span<byte>(buffer);
for (int i = 1; i <= len; i++)
{
L.GetInteger(1, i);
var value = L.CheckInteger(-1);
buffer[i - 1] = (byte)value;
PCMTo16bit(span, i, (byte)L.CheckNumber(-1));
L.Pop(1);
}
}
@ -133,7 +147,12 @@ public class Audio : IComponent
L.Error(ex.Message);
}
return 0;
var playTime = _game.Audio.HQChannel.GetSampleDuration(buffer.Length);
L.PushBoolean(true);
L.PushNumber(playTime.TotalSeconds);
return 2;
}
private static int L_Beep(IntPtr state)

View file

@ -20,7 +20,7 @@ using System;
namespace Capy64.Runtime.Libraries;
public class Event : IComponent
public class EventLib : IComponent
{
private const int MaxPushQueue = 64;
private static int PushQueue = 0;
@ -29,24 +29,14 @@ public class Event : IComponent
private static bool FrozenTaskAwaiter = false;
private static IGame _game;
public Event(IGame game)
private static Capy64 _game;
public EventLib(Capy64 game)
{
_game = game;
}
private static LuaRegister[] EventLib = new LuaRegister[]
private static readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
name = "pull",
function = L_Pull,
},
new()
{
name = "pullRaw",
function = L_PullRaw
},
new()
{
name = "push",
@ -88,54 +78,10 @@ public class Event : IComponent
private static int OpenLib(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(EventLib);
L.NewLib(Library);
return 1;
}
private static int LK_Pull(IntPtr state, int status, IntPtr ctx)
{
var L = Lua.FromIntPtr(state);
if (L.ToString(1) == "interrupt")
{
L.Error("interrupt");
}
var nargs = L.GetTop();
return nargs;
}
public static int L_Pull(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var nargs = L.GetTop();
for (int i = 1; i <= nargs; i++)
{
L.CheckString(i);
}
L.YieldK(nargs, 0, LK_Pull);
return 0;
}
private static int L_PullRaw(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var nargs = L.GetTop();
for (int i = 1; i <= nargs; i++)
{
L.CheckString(i);
}
L.Yield(nargs);
return 0;
}
private static int L_Push(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -192,11 +138,15 @@ public class Event : IComponent
{
var L = Lua.FromIntPtr(state);
var task = TaskMeta.CheckTask(L, false);
L.CheckType(1, LuaType.Table);
L.GetField(1, "task");
var task = TaskMeta.CheckTask(L, -1, false);
L.CheckAny(2);
L.ArgumentCheck(!L.IsNil(2), 2, "value cannot be nil");
if(!task.UserTask)
if (!task.UserTask)
{
L.Error("attempt to fulfill machine task");
}
@ -218,7 +168,11 @@ public class Event : IComponent
{
var L = Lua.FromIntPtr(state);
var task = TaskMeta.CheckTask(L, false);
L.CheckType(1, LuaType.Table);
L.GetField(1, "task");
var task = TaskMeta.CheckTask(L, -1, false);
var error = L.CheckString(2);
if (!task.UserTask)
@ -226,7 +180,7 @@ public class Event : IComponent
L.Error("attempt to reject machine task");
}
if(task.Status != TaskMeta.TaskStatus.Running)
if (task.Status != TaskMeta.TaskStatus.Running)
{
L.Error("attempt to reject a finished task");
}

View file

@ -24,11 +24,11 @@ using System.Linq;
namespace Capy64.Runtime.Libraries;
public class FileSystem : IComponent
public class FileSystemLib : IComponent
{
public static string DataPath = Path.Combine(Capy64.AppDataPath, "data");
public FileSystem()
public FileSystemLib()
{
if (!Directory.Exists(DataPath))
{
@ -37,7 +37,7 @@ public class FileSystem : IComponent
}
// functions to add to the library, always end libraries with null
private LuaRegister[] FsLib = new LuaRegister[] {
private readonly LuaRegister[] Library = new LuaRegister[] {
new()
{
name = "list",
@ -111,6 +111,8 @@ public class FileSystem : IComponent
new(), // NULL
};
public FileSystemLib(Capy64 _) { }
public void LuaInit(Lua state)
{
// Add "fs" library to lua, not global (uses require())
@ -120,7 +122,7 @@ public class FileSystem : IComponent
private int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(FsLib);
l.NewLib(Library);
return 1;
}
@ -132,6 +134,9 @@ public class FileSystem : IComponent
// Get drive root (C:\ for Windows, / for *nix)
var rootPath = Path.GetFullPath(Path.GetPathRoot("/") ?? "/");
var invalidPathChars = Path.GetInvalidPathChars();
path = invalidPathChars.Aggregate(path, (current, invalidChar) => current.Replace(invalidChar, ' '));
// Join path to rootPath and resolves to absolute path
// Relative paths are resolved here (es. ../ and ./)
var absolutePath = Path.GetFullPath(path, rootPath);
@ -426,19 +431,26 @@ public class FileSystem : IComponent
L.Error("path not found");
}
var attr = File.GetAttributes(path);
if (attr.HasFlag(FileAttributes.Directory))
try
{
if (!recursive && Directory.GetFileSystemEntries(path).Any())
var attr = File.GetAttributes(path);
if (attr.HasFlag(FileAttributes.Directory))
{
L.Error("directory not empty");
return 0;
if (!recursive && Directory.GetFileSystemEntries(path).Any())
{
L.Error("directory not empty");
return 0;
}
Directory.Delete(path, recursive);
}
else
{
File.Delete(path);
}
Directory.Delete(path, recursive);
}
else
catch (Exception e)
{
File.Delete(path);
return L.Error(e.Message);
}
return 0;

View file

@ -14,30 +14,27 @@
// limitations under the License.
using Capy64.API;
using Capy64.Core;
using Capy64.Runtime.Objects;
using KeraLua;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Capy64.Runtime.Libraries;
public class GPU : IComponent
public class GPULib : IComponent
{
private static IGame _game;
public GPU(IGame game)
private static Capy64 _game;
public GPULib(Capy64 game)
{
_game = game;
}
private LuaRegister[] gpuLib = new LuaRegister[] {
private readonly LuaRegister[] Library = new LuaRegister[] {
new()
{
name = "getSize",
@ -49,6 +46,11 @@ public class GPU : IComponent
function = L_SetSize,
},
new()
{
name = "isResizable",
function = L_IsResizable,
},
new()
{
name = "getPixel",
function = L_GetPixel,
@ -119,6 +121,11 @@ public class GPU : IComponent
function = L_NewBuffer,
},
new()
{
name = "bufferFrom",
function = L_BufferFrom,
},
new()
{
name = "drawBuffer",
function = L_DrawBuffer,
@ -144,14 +151,12 @@ public class GPU : IComponent
public int OpenLib(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(gpuLib);
l.NewLib(Library);
return 1;
}
public static void GetColor(uint c, out byte r, out byte g, out byte b)
{
/*if (_game.EngineMode == EngineMode.Classic)
c = ColorPalette.GetColor(c);*/
Utils.UnpackRGB(c, out r, out g, out b);
}
@ -171,8 +176,7 @@ public class GPU : IComponent
if (_game.EngineMode == EngineMode.Classic)
{
L.PushBoolean(false);
return 1;
return L.Error("Screen is not resizable");
}
var w = L.CheckInteger(1);
@ -188,6 +192,15 @@ public class GPU : IComponent
return 1;
}
private static int L_IsResizable(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushBoolean(_game.EngineMode != EngineMode.Classic);
return 1;
}
private static int L_GetPixel(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -287,7 +300,7 @@ public class GPU : IComponent
{
var L = Lua.FromIntPtr(state);
var x1 = (int)L.CheckNumber(1);
var x1 = (int)L.CheckNumber(1) - 1; // do not question, please
var y1 = (int)L.CheckNumber(2);
var x2 = (int)L.CheckNumber(3);
var y2 = (int)L.CheckNumber(4);
@ -460,6 +473,57 @@ public class GPU : IComponent
return 1;
}
private static int L_BufferFrom(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.CheckType(1, LuaType.Table);
var width = (int)L.CheckInteger(2);
var height = (int)L.CheckInteger(3);
if (width <= 0)
{
return L.ArgumentError(2, "width must be a positive integer.");
}
if (height <= 0)
{
return L.ArgumentError(3, "height must be a positive integer.");
}
var buffer = new uint[width * height];
var tableSize = L.RawLen(1);
L.ArgumentCheck(tableSize == buffer.Length, 1, "table length does not match buffer size");
for (int i = 1; i <= tableSize; i++)
{
L.GetInteger(1, i);
var value = (uint)L.CheckInteger(-1);
L.Pop(1);
// ARGB to ABGR
value =
(value & 0xFF_00_00_00U) |
((value & 0x00_FF_00_00U) >> 16) | // move R
(value & 0x00_00_FF_00U) | // move G
((value & 0x00_00_00_FFU) << 16); // move B
buffer[i - 1] = value;
}
var gpuBuffer = new GPUBufferMeta.GPUBuffer
{
Buffer = buffer,
Width = width,
Height = height,
};
ObjectManager.PushObject(L, gpuBuffer);
L.SetMetaTable(GPUBufferMeta.ObjectType);
return 1;
}
private static int L_DrawBuffer(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -469,13 +533,107 @@ public class GPU : IComponent
var x = (int)L.CheckInteger(2) - 1;
var y = (int)L.CheckInteger(3) - 1;
Rectangle? source = null;
Color color = Color.White;
float rotation = 0;
Vector2 origin = Vector2.Zero;
Vector2 scale = Vector2.One;
SpriteEffects effects = SpriteEffects.None;
if (L.IsTable(4))
{
if (L.GetField(-1, "source") == LuaType.Table)
{
int sx, sy, sw, sh;
if (L.GetInteger(-1, 1) != LuaType.Number)
L.CheckNumber(-1);
sx = (int)L.ToNumber(-1);
L.Pop(1);
if (L.GetInteger(-1, 2) != LuaType.Number)
L.CheckNumber(-1);
sy = (int)L.ToNumber(-1);
L.Pop(1);
if (L.GetInteger(-1, 3) != LuaType.Number)
L.CheckNumber(-1);
sw = (int)L.ToNumber(-1);
L.Pop(1);
if (L.GetInteger(-1, 4) != LuaType.Number)
L.CheckNumber(-1);
sh = (int)L.ToNumber(-1);
L.Pop(1);
source = new(sx, sy, sw, sh);
}
L.Pop(1);
if (L.GetField(-1, "color") == LuaType.Number)
{
var c = (uint)L.ToNumber(-1);
GetColor(c, out var r, out var g, out var b);
color = new Color(r, g, b);
}
L.Pop(1);
if (L.GetField(-1, "rotation") == LuaType.Number)
{
rotation = (float)L.ToNumber(-1);
}
L.Pop(1);
if (L.GetField(-1, "origin") == LuaType.Table)
{
int ox, oy;
if (L.GetInteger(-1, 1) != LuaType.Number)
L.CheckNumber(-1);
ox = (int)L.ToNumber(-1);
L.Pop(1);
if (L.GetInteger(-1, 2) != LuaType.Number)
L.CheckNumber(-1);
oy = (int)L.ToNumber(-1);
L.Pop(1);
origin = new Vector2(ox, oy);
}
L.Pop(1);
if (L.GetField(-1, "scale") == LuaType.Table)
{
float sx = 1;
float sy = 1;
if (L.GetInteger(-1, 1) == LuaType.Number)
sx = (float)L.ToNumber(-1);
L.Pop(1);
if (L.GetInteger(-1, 2) == LuaType.Number)
sy = (float)L.ToNumber(-1);
L.Pop(1);
scale = new(sx, sy);
}
L.Pop(1);
if (L.GetField(-1, "effects") == LuaType.Number)
{
var flags = L.CheckInteger(-1);
effects = (SpriteEffects)flags;
}
L.Pop(1);
}
_game.Drawing.DrawBuffer(buffer.Buffer, new()
{
X = x,
Y = y,
Width = buffer.Width,
Height = buffer.Height,
});
}, source, color, rotation, origin, scale, effects);
return 0;
}
@ -486,7 +644,7 @@ public class GPU : IComponent
var path = L.CheckString(1);
path = FileSystem.Resolve(path);
path = FileSystemLib.Resolve(path);
if (!File.Exists(path))
{

View file

@ -25,21 +25,20 @@ using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Capy64.Runtime.Libraries;
#nullable enable
public class HTTP : IComponent
public class HTTPLib : IComponent
{
private static IGame _game;
private static HttpClient _httpClient;
private static Capy64 _game = null!;
private static HttpClient _httpClient = null!;
private static long _requestId;
public static readonly HashSet<WebSocketClient.Client> WebSocketConnections = new();
public static readonly string UserAgent = $"Capy64/{Capy64.Version}";
private static IConfiguration _configuration;
private readonly LuaRegister[] HttpLib = new LuaRegister[]
private static IConfiguration _configuration = null!;
private readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
@ -58,13 +57,13 @@ public class HTTP : IComponent
},
new(),
};
public HTTP(IGame game, IConfiguration configuration)
public HTTPLib(Capy64 game)
{
_game = game;
_requestId = 0;
_httpClient = new();
_httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
_configuration = configuration;
_configuration = game.Configuration;
}
public void LuaInit(Lua L)
@ -76,7 +75,7 @@ public class HTTP : IComponent
private int Open(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(HttpLib);
L.NewLib(Library);
return 1;
}
@ -171,7 +170,7 @@ public class HTTP : IComponent
options["method"] = L.CheckString(-2);
break;
case "binary":
options["binary"] = L.IsBoolean(-2) ? L.ToBoolean(-2) : false;
options["binary"] = L.ToBoolean(-2);
break;
}

View file

@ -22,15 +22,15 @@ using System;
namespace Capy64.Runtime.Libraries;
public class Machine : IComponent
public class MachineLib : IComponent
{
private static IGame _game;
public Machine(IGame game)
private static Capy64 _game;
public MachineLib(Capy64 game)
{
_game = game;
}
private static LuaRegister[] MachineLib = new LuaRegister[]
private static readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
@ -83,7 +83,7 @@ public class Machine : IComponent
private static int OpenLib(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(MachineLib);
L.NewLib(Library);
return 1;
}
@ -94,10 +94,14 @@ public class Machine : IComponent
return 0;
}
private static int L_Reboot(IntPtr _)
private static int L_Reboot(IntPtr state)
{
var L = Lua.FromIntPtr(state);
RuntimeManager.Reboot();
L.Yield(0);
return 0;
}

View file

@ -1,102 +0,0 @@
// This file is part of Capy64 - https://github.com/Ale32bit/Capy64
// Copyright 2023 Alessandro "AlexDevs" Proto
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64.API;
using Capy64.Runtime.Objects;
using KeraLua;
using System.IO;
using System.Net.Sockets;
namespace Capy64.Runtime.Libraries;
public class TCP : IComponent
{
private static int Counter = 0;
private static IGame _game;
public TCP(IGame game)
{
_game = game;
Counter = 0;
}
private static LuaRegister[] TCPLib = new LuaRegister[]
{
new()
{
name = "connectAsync",
function = L_Connect,
},
new(),
};
public void LuaInit(Lua L)
{
//L.RequireF("tcp", OpenLib, false);
}
public int OpenLib(nint state)
{
var L = Lua.FromIntPtr(state);
L.NewLib(TCPLib);
return 1;
}
private static int L_Connect(nint state)
{
var L = Lua.FromIntPtr(state);
var host = L.CheckString(1);
var port = (int)L.CheckInteger(2);
L.ArgumentCheck(port >= 0 && port <= 0xffff, 2, "port must be in range 0-65535");
var client = new TcpClient();
var id = Counter++;
var task = client.ConnectAsync(host, port);
task.ContinueWith(t =>
{
if (client.Connected)
{
_game.LuaRuntime.QueueEvent("tcp_connect", LK =>
{
/*var handle = new FileHandle
{
Stream = client.GetStream(),
DefaultSize = client.ReceiveBufferSize,
};
LK.PushInteger(id);
ObjectManager.PushObject(L, handle);
L.SetMetaTable("file");*/
return 2;
});
}
else
{
_game.LuaRuntime.QueueEvent("tcp_failure", LK =>
{
LK.PushInteger(id);
LK.PushString(t.Exception.Message);
return 2;
});
}
});
L.PushInteger(id);
return 1;
}
}

View file

@ -14,7 +14,6 @@
// limitations under the License.
using Capy64.API;
using Capy64.Core;
using Capy64.Eventing.Events;
using KeraLua;
using Microsoft.Xna.Framework;
@ -24,7 +23,7 @@ using static Capy64.Utils;
namespace Capy64.Runtime.Libraries;
internal class Term : IComponent
internal class TermLib : IComponent
{
private struct Char
{
@ -50,11 +49,11 @@ internal class Term : IComponent
public static Color BackgroundColor { get; set; }
private static Char?[] CharGrid;
private static IGame _game;
private static Capy64 _game;
private static bool cursorState = false;
private static bool enableCursor = true;
private static Texture2D cursorTexture;
public Term(IGame game)
public TermLib(Capy64 game)
{
_game = game;
@ -73,7 +72,7 @@ internal class Term : IComponent
_game.EventEmitter.OnScreenSizeChange += OnScreenSizeChange;
}
private LuaRegister[] TermLib = new LuaRegister[]
private readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
@ -100,6 +99,10 @@ internal class Term : IComponent
name = "setSize",
function = L_SetSize,
},
new() {
name = "isResizable",
function = L_IsResizable,
},
new()
{
name = "getForeground",
@ -171,7 +174,7 @@ internal class Term : IComponent
public int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(TermLib);
l.NewLib(Library);
return 1;
}
@ -217,12 +220,12 @@ internal class Term : IComponent
_game.Drawing.DrawString(charpos, ch.ToString(), fg);
}
catch (ArgumentException ex) // UTF-16 fuckery
catch (ArgumentException) // UTF-16 fuckery
{
_game.Drawing.DrawString(charpos, "\xFFFD", fg);
}
if(underline)
if (underline)
{
_game.Drawing.DrawLine(charpos + new Vector2(0, CharHeight), charpos + new Vector2(CharWidth, CharHeight), fg);
}
@ -313,7 +316,7 @@ internal class Term : IComponent
if (cursorState)
{
var realpos = ToRealPos(CursorPosition - Vector2.One);
var charpos = (realpos * _game.Scale) + (CharOffset + new Vector2(0, 2)) * _game.Scale;
var charpos = (realpos * _game.Scale) + ((CharOffset + new Vector2(0, 2)) * _game.Scale);
charpos += new Vector2(Capy64.Instance.Borders.Left, Capy64.Instance.Borders.Top);
_game.Game.SpriteBatch.Draw(cursorTexture, charpos, null, ForegroundColor, 0f, Vector2.Zero, _game.Scale, SpriteEffects.None, 0);
}
@ -396,10 +399,9 @@ internal class Term : IComponent
{
var L = Lua.FromIntPtr(state);
if(_game.EngineMode == EngineMode.Classic)
if (_game.EngineMode == EngineMode.Classic)
{
L.PushBoolean(false);
return 1;
return L.Error("Terminal is not resizable");
}
var w = (int)L.CheckNumber(1);
@ -420,6 +422,15 @@ internal class Term : IComponent
return 1;
}
private static int L_IsResizable(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushBoolean(_game.EngineMode != EngineMode.Classic);
return 1;
}
private static int L_GetForegroundColor(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -538,8 +549,6 @@ internal class Term : IComponent
private static int L_Clear(IntPtr state)
{
var L = Lua.FromIntPtr(state);
Clear();
return 0;
@ -553,8 +562,6 @@ internal class Term : IComponent
private static int L_ClearLine(IntPtr state)
{
var L = Lua.FromIntPtr(state);
ClearLine();
return 0;

View file

@ -21,9 +21,15 @@ using System.Collections.Concurrent;
namespace Capy64.Runtime.Libraries;
class Timer : IComponent
class TimerLib : IComponent
{
private LuaRegister[] TimerLib = new LuaRegister[]
public class Timer
{
public int RemainingTicks = 0;
public TaskMeta.RuntimeTask Task = null!;
}
private readonly LuaRegister[] Library = new LuaRegister[]
{
new()
{
@ -44,25 +50,54 @@ class Timer : IComponent
new(),
};
private static IGame _game;
private static Capy64 _game;
private static uint _timerId = 0;
private static ConcurrentDictionary<uint, System.Timers.Timer> timers = new();
public Timer(IGame game)
private static readonly ConcurrentDictionary<uint, Timer> timers = new();
public TimerLib(Capy64 game)
{
_game = game;
_game.EventEmitter.OnTick += OnTick;
}
private void OnTick(object sender, Eventing.Events.TickEvent e)
{
if (e.IsActiveTick)
{
foreach (var t in timers)
{
var timer = t.Value;
timer.RemainingTicks--;
if (timer.RemainingTicks <= 0)
{
if (timer.Task == null)
{
_game.LuaRuntime.QueueEvent("timer", lk =>
{
lk.PushInteger(t.Key);
return 1;
});
}
else
{
timer.Task.Fulfill(lk =>
{
lk.PushInteger(t.Key);
});
}
timers.TryRemove(t.Key, out _);
}
}
}
}
public void LuaInit(Lua state)
{
_timerId = 0;
foreach (var pair in timers)
{
pair.Value.Stop();
pair.Value.Dispose();
}
timers.Clear();
state.RequireF("timer", Open, false);
@ -71,36 +106,23 @@ class Timer : IComponent
private int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(TimerLib);
l.NewLib(Library);
return 1;
}
private static int L_StartTimer(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var delay = L.CheckNumber(1);
L.ArgumentCheck(delay > 0, 1, "delay must be greater than 0");
var timerId = _timerId++;
var timer = new System.Timers.Timer
timers[timerId] = new Timer
{
AutoReset = false,
Enabled = true,
Interval = delay,
};
timers[timerId] = timer;
timer.Elapsed += (o, e) =>
{
_game.LuaRuntime.QueueEvent("timer", lk =>
{
lk.PushInteger(timerId);
return 1;
});
timers.TryRemove(timerId, out _);
RemainingTicks = (int)(delay * Capy64.Instance.TickRate)
};
L.PushInteger(timerId);
@ -112,26 +134,15 @@ class Timer : IComponent
var L = Lua.FromIntPtr(state);
var delay = L.CheckNumber(1);
L.ArgumentCheck(delay > 0, 1, "delay must be greater than 0");
var task = TaskMeta.Push(L, "timer");
var timerId = _timerId++;
var timer = new System.Timers.Timer
{
AutoReset = false,
Enabled = true,
Interval = delay,
};
timers[timerId] = timer;
timer.Elapsed += (o, e) =>
timers[timerId] = new Timer
{
task.Fulfill(lk => {
lk.PushInteger(timerId);
});
timers.TryRemove(timerId, out _);
RemainingTicks = (int)(delay * Capy64.Instance.TickRate),
Task = task,
};
return 1;

View file

@ -60,7 +60,6 @@ public class LuaState : IDisposable
if (yieldTimedOut)
{
L.Error("no yield timeout");
Console.WriteLine("tick");
}
}

View file

@ -23,25 +23,39 @@ namespace Capy64.Runtime;
public class ObjectManager : IComponent
{
private static ConcurrentDictionary<nint, object> _objects = new();
private static readonly ConcurrentDictionary<nint, object> _objects = new();
private static IGame _game;
public ObjectManager(IGame game)
private static Capy64 _game;
public ObjectManager(Capy64 game)
{
_game = game;
_game.EventEmitter.OnClose += OnClose;
}
public static void PushObject<T>(Lua L, T obj)
public static nint PushObject<T>(Lua L, T obj)
{
if (obj == null)
{
L.PushNil();
return;
return nint.Zero;
}
var p = L.NewUserData(1);
_objects[p] = obj;
return p;
}
public static T GetObject<T>(nint address, bool freeGCHandle = false)
{
if (!_objects.ContainsKey(address))
return default(T);
var reference = (T)_objects[address];
if (freeGCHandle)
_objects.Remove(address, out _);
return reference;
}
public static T ToObject<T>(Lua L, int index, bool freeGCHandle = true)

View file

@ -16,7 +16,11 @@
using Capy64.API;
using KeraLua;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using static Capy64.Runtime.Constants;
namespace Capy64.Runtime.Objects;
@ -24,7 +28,7 @@ public class FileHandle : IComponent
{
public const string ObjectType = "file";
private static LuaRegister[] Methods = new LuaRegister[]
private static readonly LuaRegister[] Methods = new LuaRegister[]
{
new()
{
@ -59,7 +63,7 @@ public class FileHandle : IComponent
new(),
};
private static LuaRegister[] MetaMethods = new LuaRegister[]
private static readonly LuaRegister[] MetaMethods = new LuaRegister[]
{
new()
{
@ -84,6 +88,8 @@ public class FileHandle : IComponent
new(),
};
public FileHandle(Capy64 _) { }
public void LuaInit(Lua L)
{
CreateMeta(L);
@ -133,7 +139,16 @@ public class FileHandle : IComponent
private static bool ReadNumber(Lua L, Stream stream)
{
return false;
var str = ReadHelper.ReadNumber(stream);
if (L.StringToNumber(str))
{
return true;
}
else
{
L.PushNil();
return false;
}
}
private static bool ReadLine(Lua L, Stream stream, bool chop)
@ -187,53 +202,66 @@ public class FileHandle : IComponent
return 2;
}
return G_Read(L, stream, 2);
}
private static int G_Read(Lua L, Stream f, int first)
{
var nargs = L.GetTop() - 1;
if (nargs == 0)
{
L.PushString("l");
nargs = 1;
}
int n;
bool success;
for (int i = 2; i <= nargs + 1; i++)
if (nargs == 0) // no arguments?
{
bool success;
if (L.Type(i) == LuaType.Number)
success = ReadLine(L, f, true);
n = first + 1; // to return 1 result
}
else
{
// ensure stack space for all results and for auxlib's buffer
L.CheckStack(nargs + MINSTACK, "too many arguments");
success = true;
for (n = first; (nargs-- > 0) && success; n++)
{
success = ReadChars(L, stream, (int)L.ToNumber(2));
}
else
{
var p = L.CheckString(i);
var mode = CheckMode(p);
switch (mode)
if (L.Type(n) == LuaType.Number)
{
case 'n':
success = ReadNumber(L, stream);
break;
case 'l':
success = ReadLine(L, stream, true);
break;
case 'L':
success = ReadLine(L, stream, false);
break;
case 'a':
ReadAll(L, stream);
success = true;
break;
default:
return L.ArgumentError(i, "invalid format");
var l = (int)L.CheckInteger(n);
success = (l == 0) ? f.Position == f.Length : ReadChars(L, f, l);
}
else
{
var p = L.CheckString(n);
var mode = CheckMode(p);
switch (mode)
{
case 'n': // number
success = ReadNumber(L, f);
break;
case 'l': // line
success = ReadLine(L, f, true);
break;
case 'L': // line with end-of-line
success = ReadLine(L, f, false);
break;
case 'a': // file
ReadAll(L, f); // read entire file
success = true; // always success
break;
default:
return L.ArgumentError(n, "invalid format");
}
}
if (!success)
{
L.Pop(1);
L.PushNil();
}
}
}
return nargs;
if (!success)
{
L.Pop(1);
L.PushNil();
}
return n - first;
}
private static int L_Write(IntPtr state)
@ -268,7 +296,6 @@ public class FileHandle : IComponent
private static int L_Lines(IntPtr state)
{
return 0;
var L = Lua.FromIntPtr(state);
var maxargn = 250;
@ -279,11 +306,51 @@ public class FileHandle : IComponent
L.PushInteger(n);
L.PushBoolean(false);
L.Rotate(2, 3);
L.PushCClosure(null, 3 + n); // todo
L.PushCClosure(IO_ReadLine, 3 + n);
return 1;
}
private static int IO_ReadLine(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var stream = ObjectManager.ToObject<Stream>(L, Lua.UpValueIndex(1), false);
int i;
int n = (int)L.ToInteger(Lua.UpValueIndex(2));
if (stream is null)
{
return L.Error("file is already closed");
}
L.SetTop(1);
L.CheckStack(n, "too many arguments");
for (i = 1; i <= n; i++)
{
L.PushCopy(Lua.UpValueIndex(3 + i));
}
n = G_Read(L, stream, 2);
Debug.Assert(n > 0);
if (L.ToBoolean(-n))
{
return n;
}
else
{
if (n > 1)
{
return L.Error(L.ToString(-n + 1));
}
if (L.ToBoolean(Lua.UpValueIndex(3)))
{
L.SetTop(0);
L.PushCopy(Lua.UpValueIndex(1));
stream.Close();
}
}
return 0;
}
private static int L_Flush(IntPtr state)
{
var L = Lua.FromIntPtr(state);
@ -348,9 +415,124 @@ public class FileHandle : IComponent
var L = Lua.FromIntPtr(state);
var stream = ToStream(L, true);
if (stream is not null)
stream.Close();
stream?.Close();
return 0;
}
private static class ReadHelper
{
static bool isdigit(char c)
{
return "0123456789"
.Contains(c, StringComparison.CurrentCultureIgnoreCase);
}
static bool isxdigit(char c)
{
return "0123456789abcdef"
.Contains(c, StringComparison.CurrentCultureIgnoreCase);
}
static bool isspace(char c)
{
return new char[] {
' ',
'\n',
'\t',
'\v',
'\f',
'\r',
}.Contains(c);
}
static bool test_eof(Lua L, Stream f)
{
L.PushString("");
return f.Position != f.Length;
}
static bool nextc(RN rn)
{
if (rn.n >= 200) // buffer overflow?
{
rn.buff[0] = '\0'; // invalidate result
return false; // fail
}
else
{
rn.buff[rn.n] = rn.c; // save current char
rn.n++;
rn.c = (char)rn.f.ReadByte(); // read next one
return true;
}
}
static bool test2(RN rn, string set)
{
if (rn.c == set[0] || rn.c == set[1])
{
return nextc(rn);
}
return false;
}
static int readdigits(RN rn, bool hex)
{
int count = 0;
while ((hex ? isxdigit(rn.c) : isdigit(rn.c)) && nextc(rn))
count++;
return count;
}
/// <summary>
/// https://www.lua.org/source/5.4/liolib.c.html#read_number
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public static string ReadNumber(Stream stream)
{
RN rn = new()
{
buff = new char[201]
};
int count = 0;
bool hex = false;
string decp = "..";
rn.f = stream;
rn.n = 0;
do
{
rn.c = (char)rn.f.ReadByte();
} while (isspace(rn.c)); /* skip spaces */
test2(rn, "+-"); /* optional sign */
if (test2(rn, "00"))
{
if (test2(rn, "xX")) hex = true; /* numeral is hexadecimal */
else count = 1; /* count initial '0' as a valid digit */
}
count += readdigits(rn, hex); // integral part
if (test2(rn, decp)) // decimal point?
count += readdigits(rn, hex); // fractional part
if (count > 0 && test2(rn, (hex ? "pP" : "eE")))
{ /* exponent mark? */
test2(rn, "-+"); /* exponent sign */
readdigits(rn, false); /* exponent digits */
}
rn.f.Position += -1;
return new string(rn.buff);
}
public class RN
{
public Stream f;
public char c;
public int n;
public char[] buff;
}
}
}

View file

@ -14,7 +14,6 @@
// limitations under the License.
using Capy64.API;
using Capy64.Core;
using KeraLua;
using System;
@ -31,7 +30,7 @@ public class GPUBufferMeta : IComponent
public int Height { get; set; }
}
private static LuaRegister[] MetaMethods = new LuaRegister[]
private static readonly LuaRegister[] MetaMethods = new LuaRegister[]
{
new()
{
@ -67,8 +66,8 @@ public class GPUBufferMeta : IComponent
new(),
};
private static IGame _game;
public GPUBufferMeta(IGame game)
private static Capy64 _game;
public GPUBufferMeta(Capy64 game)
{
_game = game;
}
@ -138,8 +137,9 @@ public class GPUBufferMeta : IComponent
var value = buffer.Buffer[key];
// ABGR to RGB
// ABGR to ARGB
value =
(value & 0xFF_00_00_00U) |
((value & 0x00_00_00_FFU) << 16) | // move R
(value & 0x00_00_FF_00U) | // move G
((value & 0x00_FF_00_00U) >> 16); // move B
@ -174,13 +174,12 @@ public class GPUBufferMeta : IComponent
var value = (uint)L.ToInteger(3);
value = GetColor(value);
// RGB to ABGR
// ARGB to ABGR
value =
(value & 0xFF_00_00_00U) |
((value & 0x00_FF_00_00U) >> 16) | // move R
(value & 0x00_00_FF_00U) | // move G
((value & 0x00_00_00_FFU) << 16) | // move B
0xFF_00_00_00U;
((value & 0x00_00_00_FFU) << 16); // move B
buffer.Buffer[key] = value;

View file

@ -16,10 +16,6 @@
using Capy64.API;
using KeraLua;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.Runtime.Objects;
@ -33,18 +29,18 @@ public class Socket : IDisposable
public class SocketLib : IComponent
{
private static IGame _game;
public SocketLib(IGame game)
private static Capy64 _game = null!;
public SocketLib(Capy64 game)
{
_game = game;
}
private static LuaRegister[] Methods = new LuaRegister[] {
private static readonly LuaRegister[] Methods = new LuaRegister[] {
new(),
};
private static LuaRegister[] MetaMethods = new LuaRegister[] {
private static readonly LuaRegister[] MetaMethods = new LuaRegister[] {
new()
{
name = "__index",

View file

@ -14,22 +14,15 @@
// limitations under the License.
using Capy64.API;
using Cyotek.Drawing.BitmapFont;
using KeraLua;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Capy64.Runtime.Objects;
public class TaskMeta : IComponent
{
private static IGame _game;
public TaskMeta(IGame game)
private static Capy64 _game;
public TaskMeta(Capy64 game)
{
_game = game;
}
@ -50,6 +43,7 @@ public class TaskMeta : IComponent
}
public Guid Guid { get; set; } = Guid.NewGuid();
public nint Pointer { get; set; } = nint.Zero;
public string Name { get; set; }
public TaskStatus Status { get; set; } = TaskStatus.Running;
public string Error { get; private set; }
@ -69,7 +63,7 @@ public class TaskMeta : IComponent
var container = tasks.NewThread();
lk(container);
if(container.IsNoneOrNil(-1))
if (container.IsNoneOrNil(-1))
{
throw new Exception("Task result cannot be nil");
}
@ -86,7 +80,6 @@ public class TaskMeta : IComponent
DataIndex = tasks.GetTop();
}
_game.LuaRuntime.QueueEvent("task_finish", LK =>
{
LK.PushString(Guid.ToString());
@ -123,7 +116,7 @@ public class TaskMeta : IComponent
}
}
private static LuaRegister[] Methods = new LuaRegister[]
private static readonly LuaRegister[] Methods = new LuaRegister[]
{
new()
{
@ -158,7 +151,7 @@ public class TaskMeta : IComponent
new(),
};
private static LuaRegister[] MetaMethods = new LuaRegister[]
private static readonly LuaRegister[] MetaMethods = new LuaRegister[]
{
new()
{
@ -210,26 +203,58 @@ public class TaskMeta : IComponent
var task = new RuntimeTask(typeName);
ObjectManager.PushObject(L, task);
L.NewTable();
task.Pointer = ObjectManager.PushObject(L, task);
L.SetMetaTable(ObjectType);
L.SetField(-2, "task");
L.SetMetaTable(ObjectType);
return task;
}
public static RuntimeTask ToTask(Lua L, bool gc = false)
public static RuntimeTask ToTask(Lua L, int index = 1, bool gc = false)
{
return ObjectManager.ToObject<RuntimeTask>(L, 1, gc);
RuntimeTask task;
if (L.Type(index) != LuaType.Table)
{
if (L.TestUserData(index, ObjectType) == nint.Zero)
{
return null;
}
else
{
task = ObjectManager.ToObject<RuntimeTask>(L, index, gc);
}
}
else
{
L.GetField(index, "task");
task = task = ObjectManager.ToObject<RuntimeTask>(L, -1, gc);
}
return task;
}
public static RuntimeTask CheckTask(Lua L, bool gc = false)
public static RuntimeTask CheckTask(Lua L, int index = 1, bool gc = false)
{
var obj = ObjectManager.CheckObject<RuntimeTask>(L, 1, ObjectType, gc);
if (obj is null)
RuntimeTask task;
if (L.Type(index) != LuaType.Table)
{
task = ObjectManager.CheckObject<RuntimeTask>(L, index, ObjectType, gc);
}
else
{
L.GetField(index, "task");
task = ObjectManager.CheckObject<RuntimeTask>(L, -1, ObjectType, gc);
}
if (task is null)
{
L.Error("attempt to use a closed task");
return null;
}
return obj;
return task;
}
private static int FindSpot()
@ -262,7 +287,9 @@ public class TaskMeta : IComponent
private static void WaitForTask(Lua L)
{
L.PushCFunction(Libraries.Event.L_Pull);
L.PushString("coroutine");
L.GetTable(-1);
L.GetField(-1, "yield");
L.PushString("task_finish");
L.CallK(1, 4, 0, LK_Await);
}
@ -273,7 +300,7 @@ public class TaskMeta : IComponent
L.Warning("Native task awaiter should be avoided", false);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
if (task.Status == TaskStatus.Succeeded)
{
@ -300,7 +327,9 @@ public class TaskMeta : IComponent
private static int LK_Await(IntPtr state, int status, nint ctx)
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
var taskId = L.CheckString(3);
if (task.Guid.ToString() != taskId)
@ -316,7 +345,7 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
L.PushString(task.Guid.ToString());
@ -327,7 +356,7 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
L.PushString(task.Name);
@ -338,7 +367,7 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
L.PushString(task.Status.ToString().ToLower());
@ -349,7 +378,7 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
if (task.Status == TaskStatus.Succeeded)
{
@ -369,7 +398,7 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = CheckTask(L, false);
var task = CheckTask(L, 1, false);
if (task.Status == TaskStatus.Failed)
{
@ -387,7 +416,11 @@ public class TaskMeta : IComponent
{
var L = Lua.FromIntPtr(state);
var task = ToTask(L, true);
L.CheckType(1, LuaType.Table);
L.GetField(1, "task");
var task = CheckTask(L, -1, true);
if (task is null)
return 0;
@ -405,7 +438,9 @@ public class TaskMeta : IComponent
private static int LM_ToString(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var task = ToTask(L);
L.PushString("Task<{0}>: {1} ({2})", task?.Name, task?.Guid, task?.Status);
return 1;

View file

@ -17,7 +17,6 @@ using Capy64.API;
using Capy64.Runtime.Libraries;
using KeraLua;
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Threading;
@ -74,6 +73,8 @@ public class WebSocketClient : IComponent
new(),
};
public WebSocketClient(Capy64 _) { }
public void LuaInit(Lua L)
{
CreateMeta(L);
@ -153,7 +154,7 @@ public class WebSocketClient : IComponent
});
});
HTTP.WebSocketConnections.Remove(client);
HTTPLib.WebSocketConnections.Remove(client);
return 0;
}

View file

@ -21,33 +21,33 @@ namespace Capy64.Runtime;
public class PanicScreen
{
public static Color ForegroundColor = Color.White;
public static Color BackgroundColor = new Color(0, 51, 187);
public static Color BackgroundColor = new(0, 51, 187);
public static void Render(string error, string details = null)
{
Term.ForegroundColor = ForegroundColor;
Term.BackgroundColor = BackgroundColor;
Term.SetCursorBlink(false);
Term.SetSize(57, 23);
Term.Clear();
TermLib.ForegroundColor = ForegroundColor;
TermLib.BackgroundColor = BackgroundColor;
TermLib.SetCursorBlink(false);
TermLib.SetSize(57, 23);
TermLib.Clear();
var title = " Capy64 ";
var halfX = (Term.Width / 2) + 1;
Term.SetCursorPosition(halfX - (title.Length / 2), 2);
Term.ForegroundColor = BackgroundColor;
Term.BackgroundColor = ForegroundColor;
Term.Write(title);
var halfX = (TermLib.Width / 2) + 1;
TermLib.SetCursorPosition(halfX - (title.Length / 2), 2);
TermLib.ForegroundColor = BackgroundColor;
TermLib.BackgroundColor = ForegroundColor;
TermLib.Write(title);
Term.ForegroundColor = ForegroundColor;
Term.BackgroundColor = BackgroundColor;
Term.SetCursorPosition(1, 4);
TermLib.ForegroundColor = ForegroundColor;
TermLib.BackgroundColor = BackgroundColor;
TermLib.SetCursorPosition(1, 4);
Print(error + '\n');
if (details is not null)
{
Print(details);
}
Term.SetCursorPosition(1, 19);
TermLib.SetCursorPosition(1, 23);
Print("Hold CTRL + ALT + INSERT to reboot.");
}
@ -55,12 +55,12 @@ public class PanicScreen
{
foreach (var ch in txt)
{
Term.Write(ch.ToString());
if (Term.CursorPosition.X >= Term.Width || ch == '\n')
TermLib.Write(ch.ToString());
if (TermLib.CursorPosition.X >= TermLib.Width || ch == '\n')
{
Term.SetCursorPosition(1, (int)Term.CursorPosition.Y + 1);
TermLib.SetCursorPosition(1, (int)TermLib.CursorPosition.Y + 1);
}
}
Term.SetCursorPosition(1, (int)Term.CursorPosition.Y + 1);
TermLib.SetCursorPosition(1, (int)TermLib.CursorPosition.Y + 1);
}
}

View file

@ -15,7 +15,6 @@
using Capy64.API;
using Capy64.Eventing.Events;
using Capy64.Extensions;
using Capy64.Runtime.Libraries;
using KeraLua;
using System;
@ -33,8 +32,8 @@ internal class RuntimeManager : IComponent
private static bool close = false;
private static bool inPanic = false;
private static IGame _game;
public RuntimeManager(IGame game)
private static Capy64 _game;
public RuntimeManager(Capy64 game)
{
_game = game;
@ -98,6 +97,8 @@ internal class RuntimeManager : IComponent
{
_game.Discord.SetPresence("Booting up...");
InstallOS(false);
luaState = new LuaState();
_game.LuaRuntime = luaState;
luaState.Init();
@ -112,19 +113,18 @@ internal class RuntimeManager : IComponent
emitter.Register();
LoadFirmware();
luaState.Thread.PushCFunction(L_OpenDataFolder);
luaState.Thread.SetGlobal("openDataFolder");
luaState.Thread.PushCFunction(L_ShouldInstallOS);
luaState.Thread.SetGlobal("shouldInstallOS");
luaState.Thread.PushCFunction(L_FlagInstalled);
luaState.Thread.SetGlobal("flagInstalled");
luaState.Thread.PushCFunction(L_InstallOS);
luaState.Thread.SetGlobal("installOS");
luaState.Thread.PushCFunction(L_Exit);
luaState.Thread.SetGlobal("exit");
var status = luaState.Thread.LoadFile("Assets/bios.lua");
var status = luaState.Thread.LoadFile(Path.Combine(Capy64.AssetsPath, "Lua/bios.lua"));
if (status != LuaStatus.OK)
{
throw new LuaException(luaState.Thread.ToString(-1));
@ -133,11 +133,10 @@ internal class RuntimeManager : IComponent
private void InitOS()
{
_game.Discord.SetPresence("On CapyOS");
luaState = new LuaState();
_game.LuaRuntime = luaState;
luaState.Init();
CopyHostLibraries();
emitter = new(_game.EventEmitter, luaState);
@ -149,7 +148,14 @@ internal class RuntimeManager : IComponent
emitter.Register();
var initContent = File.ReadAllText(Path.Combine(FileSystem.DataPath, "init.lua"));
LoadFirmware();
if (!File.Exists(Path.Combine(FileSystemLib.DataPath, "init.lua")))
{
throw new LuaException("Operating System not found\nMissing init.lua");
}
var initContent = File.ReadAllText(Path.Combine(FileSystemLib.DataPath, "init.lua"));
var status = luaState.Thread.LoadString(initContent, "=init.lua");
if (status != LuaStatus.OK)
{
@ -157,6 +163,36 @@ internal class RuntimeManager : IComponent
}
}
private void CopyHostLibraries()
{
// table that will contain host libraries
luaState.Thread.NewTable();
luaState.Thread.GetGlobal("package");
luaState.Thread.GetField(-1, "loaded");
luaState.Thread.PushNil();
while (luaState.Thread.Next(-2))
{
var libname = luaState.Thread.ToString(-2);
luaState.Thread.SetField(1, libname);
}
luaState.Thread.Rotate(1, -1);
luaState.Thread.SetField(1, "_host");
luaState.Thread.SetTop(0);
}
private void LoadFirmware()
{
var firmwareContent = File.ReadAllText(Path.Combine(Capy64.AssetsPath, "Lua/firmware.lua"));
var errored = luaState.Thread.DoString(firmwareContent);
if (errored)
{
throw new LuaException(luaState.Thread.ToString(-1));
}
}
public static void Reset()
{
close = true;
@ -174,9 +210,19 @@ internal class RuntimeManager : IComponent
_game.Exit();
}
public static void InstallOS(bool force = false)
{
var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed");
if (!File.Exists(installedFilePath) || force)
{
FileSystemLib.CopyDirectory(Path.Combine(Capy64.AssetsPath, "Lua/CapyOS"), FileSystemLib.DataPath, true, true);
File.Create(installedFilePath).Dispose();
}
}
private static int L_OpenDataFolder(IntPtr state)
{
var path = FileSystem.DataPath;
var path = FileSystemLib.DataPath;
switch (Environment.OSVersion.Platform)
{
case PlatformID.Win32NT:
@ -190,22 +236,9 @@ internal class RuntimeManager : IComponent
return 0;
}
private static int L_ShouldInstallOS(IntPtr state)
private static int L_InstallOS(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed");
L.PushBoolean(!File.Exists(installedFilePath));
return 1;
}
private static int L_FlagInstalled(IntPtr state)
{
var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed");
if (!File.Exists(installedFilePath))
File.Create(installedFilePath).Dispose();
InstallOS(true);
return 0;
}

View file

@ -121,6 +121,20 @@ internal class Sandbox
L.SetTable(-3);
L.Pop(1);
// Replace debug.debug with a dummy function to avoid stalling the program
L.GetGlobal("debug");
L.PushString("debug");
L.PushCFunction(L_Dummy);
L.SetTable(-3);
L.Pop(1);
}
internal static int L_Dummy(IntPtr state)
{
return 0;
}
internal static int L_Searcher(IntPtr state)
@ -192,7 +206,7 @@ internal class Sandbox
var errorMessage = new StringBuilder();
foreach (var possiblePath in possiblePaths)
{
var path = FileSystem.Resolve(possiblePath);
var path = FileSystemLib.Resolve(possiblePath);
var info = new FileInfo(path);
if (!info.Exists)
{
@ -229,7 +243,7 @@ internal class Sandbox
bool hasMode = !L.IsNone(2);
bool hasEnv = !L.IsNone(3);
var path = FileSystem.Resolve(filename);
var path = FileSystemLib.Resolve(filename);
var fileInfo = new FileInfo(path);
if (!fileInfo.Exists)

View file

@ -1,73 +0,0 @@
// This file is part of Capy64 - https://github.com/Ale32bit/Capy64
// Copyright 2023 Alessandro "AlexDevs" Proto
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Capy64;
public class Worker : IHostedService
{
private readonly IGame _game;
private readonly IHostApplicationLifetime _appLifetime;
private readonly IServiceProvider _serviceProvider;
public Worker(IGame game, IHostApplicationLifetime appLifetime, IServiceProvider serviceProvider)
{
_game = game;
_appLifetime = appLifetime;
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
_game.Exiting += OnGameExiting;
_game.ConfigureServices(_serviceProvider);
return Task.CompletedTask;
}
private void OnGameExiting(object sender, EventArgs e)
{
StopAsync(new CancellationToken());
}
public Task StopAsync(CancellationToken cancellationToken)
{
_appLifetime.StopApplication();
return Task.CompletedTask;
}
private void OnStarted()
{
_game.Run();
}
private void OnStopping()
{
}
private void OnStopped()
{
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>

View file

@ -1,13 +1,12 @@
using Capy64;
using Capy64.API;
using Capy64.API;
using KeraLua;
namespace ExamplePlugin;
public class MyPlugin : IComponent
{
private static IGame _game;
public MyPlugin(IGame game)
private static Capy64.Capy64 _game;
public MyPlugin(Capy64.Capy64 game)
{
_game = game;
}

BIN
Resources/256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

11
Resources/Capy64.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Capy64
Comment=Lua Fantasy Computer
Categories=Game;Emulator;
Icon=me.alexdevs.Capy64
Exec=Capy64
Terminal=false

37
Resources/appdata.xml Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>me.alexdevs.Capy64</id>
<launchable type="desktop-id">me.alexdevs.Capy64.desktop</launchable>
<name>Capy64</name>
<summary>Lua Fantasy Computer</summary>
<developer_name>AlexDevs</developer_name>
<url>https://capy64.alexdevs.me/</url>
<metadata_license>MIT</metadata_license>
<project_license>Apache-2.0</project_license>
<content_rating type="oars-1.1" />
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>gamepad</control>
</supports>
<description>
<p>
Capy64 is a fantasy console that runs sandbox Lua 5.4.
</p>
<p>
Create anything, from Hello Worlds to games!
</p>
</description>
<releases>
<release version="1.1.2" date="2023-07-24" type="beta">
<url>https://github.com/Ale32bit/Capy64/releases/tag/v1.1.2-beta</url>
</release>
</releases>
</component>

19
default.nix Normal file
View file

@ -0,0 +1,19 @@
{ pkgs ? import <nixpkgs> { system = builtins.currentSystem; } }:
pkgs.buildDotnetModule rec {
pname = "Capy64";
version = "1.1.0-beta";
src = ./.;
projectFile = "Capy64/Capy64.csproj";
nugetDeps = ./deps.nix;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_7_0;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
meta = with pkgs.lib; {
homepage = "https://github.com/Ale32bit/Capy64";
description = "Capy64";
license = with licenses; [ asl20 ];
};
}

53
deps.nix Normal file
View file

@ -0,0 +1,53 @@
# This file was automatically generated by passthru.fetch-deps.
# Please dont edit it manually, your changes might get overwritten!
{ fetchNuGet }: [
(fetchNuGet { pname = "Cyotek.Drawing.BitmapFont"; version = "2.0.4"; sha256 = "04n0lq9sqfjzyvkqav6qrc8v9fb7jv1n7jk0j4r6ivfbmffzq8if"; })
(fetchNuGet { pname = "DiscordRichPresence"; version = "1.1.3.18"; sha256 = "0p4bhaggjjfd4gl06yiphqgncxgcq2bws4sjkrw0n2ldf3hgrps3"; })
(fetchNuGet { pname = "FontStashSharp.Base"; version = "1.1.8"; sha256 = "00b4x3hcldp1f6g32khhyv32z127ab6rcgmd4b88k1534vv128bp"; })
(fetchNuGet { pname = "FontStashSharp.MonoGame"; version = "1.2.8"; sha256 = "0qgp1i54zm27qxilxbpznv9s3mgf831x7xz5z3w66zcp53qgyk93"; })
(fetchNuGet { pname = "FontStashSharp.Rasterizers.StbTrueTypeSharp"; version = "1.1.8"; sha256 = "1rnnwx748378hdbfvldxzacwc710ivvlcjzm9qi2pdh8qcz4v6db"; })
(fetchNuGet { pname = "KeraLua"; version = "1.3.3"; sha256 = "1y76f6582fld1jpbkawv26344p6xwdk07xj3im25p8yf2qcw8yln"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration"; version = "7.0.0"; sha256 = "0n1grglxql9llmrsbbnlz5chx8mxrb5cpvjngm0hfyrkgzcwz90d"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Abstractions"; version = "7.0.0"; sha256 = "1as8cygz0pagg17w22nsf6mb49lr2mcl1x8i3ad1wi8lyzygy1a3"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Binder"; version = "7.0.3"; sha256 = "1n59jk6kqqy5f0gfx99b4j2m2clylznvxj1dwm1fjn3gmh2pi35v"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.CommandLine"; version = "7.0.0"; sha256 = "1pmgjrvwdzqrxjb24cg3fd624r64lgywbqc9symd5hyl4175pwk8"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.EnvironmentVariables"; version = "7.0.0"; sha256 = "0nhh7rnh45s39x8sjn88czg7nyfpry85pkm0g619j8b468zj8nb4"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.FileExtensions"; version = "7.0.0"; sha256 = "1fk7dcz6gfhd1k1d8ksz22rnjvj1waqjzk29ym4i3dz73rsq8j1i"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Json"; version = "7.0.0"; sha256 = "05zjmrpp99l128wijp1fy8asskc11ls871qaqr4mjnz3gbfycxnj"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.UserSecrets"; version = "7.0.0"; sha256 = "0ks7lcyvfvr3ar36f5gp89bnnblxzic5vawppfcrvhw1ivas4mp1"; })
(fetchNuGet { pname = "Microsoft.Extensions.DependencyInjection"; version = "7.0.0"; sha256 = "121zs4jp8iimgbpzm3wsglhjwkc06irg1pxy8c1zcdlsg34cfq1p"; })
(fetchNuGet { pname = "Microsoft.Extensions.DependencyInjection.Abstractions"; version = "7.0.0"; sha256 = "181d7mp9307fs17lyy42f8cxnjwysddmpsalky4m0pqxcimnr6g7"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileProviders.Abstractions"; version = "7.0.0"; sha256 = "0ff20yklyjgyjzdyv7sybczgqhgd557m05dbwxzjznr0x41b180d"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileProviders.Physical"; version = "7.0.0"; sha256 = "1f1h0l47abw0spssd64qkhgd7b54pyzslyb586zp21milimcfmgv"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileSystemGlobbing"; version = "7.0.0"; sha256 = "1812vnkn8n0i4yr3k5azcxcfx1bbpcsmms95rdyxjfrzfksr05ai"; })
(fetchNuGet { pname = "Microsoft.Extensions.Hosting"; version = "7.0.1"; sha256 = "1044pm14ark2d7xiqll1cykawmp6m2f3ba9w6nfw0r3a2wfbmnxn"; })
(fetchNuGet { pname = "Microsoft.Extensions.Hosting.Abstractions"; version = "7.0.0"; sha256 = "1h5szfsr1dalsvdj9c18y6362853chisfns0hfpsq44hz0pr8j9q"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging"; version = "7.0.0"; sha256 = "1bqd3pqn5dacgnkq0grc17cgb2i0w8z1raw12nwm3p3zhrfcvgxf"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Abstractions"; version = "7.0.0"; sha256 = "1gn7d18i1wfy13vrwhmdv1rmsb4vrk26kqdld4cgvh77yigj90xs"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Configuration"; version = "7.0.0"; sha256 = "1f5fhpvzwyrwxh3g1ry027s4skmklf6mbm2w0p13h0x6fbmxcb24"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Console"; version = "7.0.0"; sha256 = "1m8ri2m3vlv9vzk0068jkrx0vkk4sqmk1kxmn8pc3wys38d38qaf"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Debug"; version = "7.0.0"; sha256 = "14p7hrhdd58fxdhjbyjwmlzr00vs03bmns3sf2f6alsgpvbf2h1i"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.EventLog"; version = "7.0.0"; sha256 = "0q1cgi456shngxs70ar0ibshpm5qk8whw369jrl6xdxnf1vxkkq8"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.EventSource"; version = "7.0.0"; sha256 = "11rskmrijf6xv78slm38zywj6l3wjlm017kijhan1kfg56f1kvdk"; })
(fetchNuGet { pname = "Microsoft.Extensions.Options"; version = "7.0.1"; sha256 = "0ghz4y4gxnf2vw8yvhz9nkw21p6q2qqwh19phkk1xwxywyilr3mq"; })
(fetchNuGet { pname = "Microsoft.Extensions.Options.ConfigurationExtensions"; version = "7.0.0"; sha256 = "1liyprh0zha2vgmqh92n8kkjz61zwhr7g16f0gmr297z2rg1j5pj"; })
(fetchNuGet { pname = "Microsoft.Extensions.Primitives"; version = "7.0.0"; sha256 = "1b4km9fszid9vp2zb3gya5ni9fn8bq62bzaas2ck2r7gs0sdys80"; })
(fetchNuGet { pname = "Microsoft.NETCore.Platforms"; version = "2.0.0"; sha256 = "1fk2fk2639i7nzy58m9dvpdnzql4vb8yl8vr19r2fp8lmj9w2jr0"; })
(fetchNuGet { pname = "Microsoft.Win32.Registry"; version = "4.5.0"; sha256 = "1zapbz161ji8h82xiajgriq6zgzmb1f3ar517p2h63plhsq5gh2q"; })
(fetchNuGet { pname = "MonoGame.Content.Builder.Task"; version = "3.8.1.303"; sha256 = "17r9aamc3wlxnvjf8s5ndsz9p6qynyklkwhawjkxsij2cw1x1syp"; })
(fetchNuGet { pname = "MonoGame.Extended"; version = "3.8.0"; sha256 = "0xgk6z3d091wd6naqiwlhp649qxvp113w5f0nddm02mh5f63mmxw"; })
(fetchNuGet { pname = "MonoGame.Extended.Graphics"; version = "3.8.0"; sha256 = "0kj4adbhds2mvd9z1sym35ms0kr4f4b62gnpbws2whc29nixs5fi"; })
(fetchNuGet { pname = "MonoGame.Framework.DesktopGL"; version = "3.8.1.303"; sha256 = "12x7ajkh254x6l1s3i4q0j3pm5p7n95xysix19wvx3x2y6pp10nd"; })
(fetchNuGet { pname = "Newtonsoft.Json"; version = "12.0.3"; sha256 = "17dzl305d835mzign8r15vkmav2hq8l6g7942dfjpnzr17wwl89x"; })
(fetchNuGet { pname = "Newtonsoft.Json"; version = "13.0.1"; sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb"; })
(fetchNuGet { pname = "StbImageSharp"; version = "2.27.13"; sha256 = "0zqnzgnkx3c9ycyk25jjcrl8wrg3pcgk06lxf3hvsplwxwldc8fa"; })
(fetchNuGet { pname = "StbTrueTypeSharp"; version = "1.26.11"; sha256 = "1wh2jmym3pvyw8ywrwas3qgqswc6kr4az1bxbn9vb1m3vba0qjn4"; })
(fetchNuGet { pname = "System.ComponentModel.Composition"; version = "7.0.0"; sha256 = "1gkn56gclkn6qnsvaw5fzw6qb45pa7rffxph1gyqhq7ywvmm0nc3"; })
(fetchNuGet { pname = "System.Diagnostics.DiagnosticSource"; version = "7.0.1"; sha256 = "1ajh5y33apcypz5pnvzxsx44n95ciskzpvbxk0kfg7n9li3nfs1v"; })
(fetchNuGet { pname = "System.Diagnostics.EventLog"; version = "7.0.0"; sha256 = "16p8z975dnzmncfifa9gw9n3k9ycpr2qvz7lglpghsvx0fava8k9"; })
(fetchNuGet { pname = "System.Security.AccessControl"; version = "4.5.0"; sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0"; })
(fetchNuGet { pname = "System.Security.Principal.Windows"; version = "4.5.0"; sha256 = "0rmj89wsl5yzwh0kqjgx45vzf694v9p92r4x4q6yxldk1cv1hi86"; })
(fetchNuGet { pname = "System.Text.Encodings.Web"; version = "7.0.0"; sha256 = "1151hbyrcf8kyg1jz8k9awpbic98lwz9x129rg7zk1wrs6vjlpxl"; })
(fetchNuGet { pname = "System.Text.Json"; version = "7.0.0"; sha256 = "0scb0lp7wbgcinaa4kqiqs7b8i5nx4ppfad81138jiwd1sl37pyp"; })
]