Add project files.

This commit is contained in:
Alessandro Proto 2023-01-07 16:59:47 +01:00
commit 84cba789a3
55 changed files with 7077 additions and 0 deletions

63
.gitattributes vendored Normal file
View file

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

363
.gitignore vendored Normal file
View file

@ -0,0 +1,363 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

25
Capy64.sln Normal file
View file

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Capy64", "Capy64\Capy64.csproj", "{D38FAB55-99A5-4C5B-9BF7-CC76443AC43A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D38FAB55-99A5-4C5B-9BF7-CC76443AC43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D38FAB55-99A5-4C5B-9BF7-CC76443AC43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D38FAB55-99A5-4C5B-9BF7-CC76443AC43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D38FAB55-99A5-4C5B-9BF7-CC76443AC43A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {16085476-C90F-4033-8ACB-AC42E055010D}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,36 @@
{
"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"
]
}
}
}

16
Capy64/API/IPlugin.cs Normal file
View file

@ -0,0 +1,16 @@
using KeraLua;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.API;
public interface IPlugin
{
void ConfigureServices(IServiceCollection services) { }
void LuaInit(Lua state) { }
}

446
Capy64/Assets/Lua/init.lua Normal file
View file

@ -0,0 +1,446 @@
local term = require("term")
local timer = require("timer")
os.print = print
package.path = "/lib/?.lua;/lib/?/init.lua;" .. package.path
local event = require("event")
local colors = require("colors")
local expect = require("expect").expect
function timer.sleep(n)
local timerId = timer.start(n)
repeat
local _, par = coroutine.yield("timer")
until par == timerId
end
function write(sText)
expect(1, sText, "string", "number")
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 print(...)
local nLinesPrinted = 0
local nLimit = select("#", ...)
for n = 1, nLimit do
local s = tostring(select(n, ...))
if n < nLimit then
s = s .. "\t"
end
nLinesPrinted = nLinesPrinted + write(s)
end
nLinesPrinted = nLinesPrinted + write("\n")
return nLinesPrinted
end
function 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.getForeground()
oldBg = term.getBackground()
term.setForeground(colors.white)
term.setBackground(colors.gray)
end
if sReplace then
term.write(string.rep(sReplace, #sCompletion))
else
term.write(sCompletion)
end
if not _bClear then
term.setForeground(oldText)
term.setBackground(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 param1 == "enter" then
-- Enter/Numpad Enter
if nCompletion then
clear()
uncomplete()
redraw()
end
break
elseif param1 == "left" then
-- Left
if nPos > 0 then
clear()
nPos = nPos - 1
recomplete()
redraw()
end
elseif param1 == "right" then
-- Right
if nPos < #sLine then
-- Move right
clear()
nPos = nPos + 1
recomplete()
redraw()
else
-- Accept autocomplete
acceptCompletion()
end
elseif param1 == "up" or param1 == "down" then
-- Up or down
if nCompletion then
-- Cycle completions
clear()
if param == "up" then
nCompletion = nCompletion - 1
if nCompletion < 1 then
nCompletion = #tCompletions
end
elseif param == "down" then
nCompletion = nCompletion + 1
if nCompletion > #tCompletions then
nCompletion = 1
end
end
redraw()
elseif _tHistory then
-- Cycle history
clear()
if param1 == "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 param1 == "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 param1 == "home" then
-- Home
if nPos > 0 then
clear()
nPos = 0
recomplete()
redraw()
end
elseif param1 == "delete" then
-- Delete
if nPos < #sLine then
clear()
sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
recomplete()
redraw()
end
elseif param1 == "end" then
-- End
if nPos < #sLine then
clear()
nPos = #sLine
recomplete()
redraw()
end
elseif param1 == "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 printError(...)
local r, g, b = term.getForeground()
term.setForeground(colors.red)
print(...)
term.setForeground(r, g, b)
end
local tEnv = {
["_echo"] = function(...)
return ...
end,
p = function(t, sv)
for k, v in pairs(t) do
if sv then
print(k, v)
else
print(k, type(v))
end
end
end,
fs = require("fs"),
}
setmetatable(tEnv, { __index = _ENV })
local tCommandHistory = {}
term.setSize(51, 19)
term.setForeground(colors.yellow)
term.setBackground(colors.black)
term.clear()
term.setPos(1,1)
print("Lua prompt")
while true do
term.setForeground(colors.yellow)
write("> ")
term.setForeground(colors.white)
local s = read(nil, tCommandHistory)
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s)
end
local nForcePrint = 0
local func, e = load(s, "=lua", "t", tEnv)
local func2 = load("return _echo(" .. s .. ");", "=lua", "t", tEnv)
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(tostring(value))
n = n + 1
end
else
printError(tResults[2])
end
else
printError(e)
end
end

View file

@ -0,0 +1,93 @@
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 = {
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
return colors;

View file

@ -0,0 +1,15 @@
local event = {}
function event.pull(...)
local pars = table.pack(event.pullRaw(...))
if pars[1] == "interrupt" then
error("Interrupted", 0)
end
return table.unpack(pars)
end
function event.pullRaw(...)
return coroutine.yield(...)
end
return event

View file

@ -0,0 +1,123 @@
--[[- The @{cc.expect} library provides helper functions for verifying that
function arguments are well-formed and of the correct type.
@module cc.expect
@since 1.84.0
@changed 1.96.0 The module can now be called directly as a function, which wraps around `expect.expect`.
@usage Define a basic function and check it has the correct arguments.
local expect = require "cc.expect"
local expect, field = expect.expect, expect.field
local function add_person(name, info)
expect(1, name, "string")
expect(2, info, "table", "nil")
if info then
print("Got age=", field(info, "age", "number"))
print("Got gender=", field(info, "gender", "string", "nil"))
end
end
add_person("Anastazja") -- `info' is optional
add_person("Kion", { age = 23 }) -- `gender' is optional
add_person("Caoimhin", { age = 23, gender = true }) -- error!
]]
local native_select, native_type = select, type
local function get_type_names(...)
local types = table.pack(...)
for i = types.n, 1, -1 do
if types[i] == "nil" then table.remove(types, i) end
end
if #types <= 1 then
return tostring(...)
else
return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types]
end
end
--- Expect an argument to have a specific type.
--
-- @tparam number index The 1-based argument index.
-- @param value The argument's value.
-- @tparam string ... The allowed types of the argument.
-- @return The given `value`.
-- @throws If the value is not one of the allowed types.
local function expect(index, value, ...)
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return value end
end
-- If we can determine the function name with a high level of confidence, try to include it.
local name
local ok, info = pcall(debug.getinfo, 3, "nS")
if ok and info.name and info.name ~= "" and info.what ~= "C" then name = info.name end
local type_names = get_type_names(...)
if name then
error(("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3)
else
error(("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3)
end
end
--- Expect an field to have a specific type.
--
-- @tparam table tbl The table to index.
-- @tparam string index The field name to check.
-- @tparam string ... The allowed types of the argument.
-- @return The contents of the given field.
-- @throws If the field is not one of the allowed types.
local function field(tbl, index, ...)
expect(1, tbl, "table")
expect(2, index, "string")
local value = tbl[index]
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return value end
end
if value == nil then
error(("field '%s' missing from table"):format(index), 3)
else
error(("bad field '%s' (expected %s, got %s)"):format(index, get_type_names(...), t), 3)
end
end
local function is_nan(num)
return num ~= num
end
--- Expect a number to be within a specific range.
--
-- @tparam number num The value to check.
-- @tparam number min The minimum value, if nil then `-math.huge` is used.
-- @tparam number max The maximum value, if nil then `math.huge` is used.
-- @return The given `value`.
-- @throws If the value is outside of the allowed range.
-- @since 1.96.0
local function range(num, min, max)
expect(1, num, "number")
min = expect(2, min, "number", "nil") or -math.huge
max = expect(3, max, "number", "nil") or math.huge
if min > max then
error("min must be less than or equal to max)", 2)
end
if is_nan(num) or num < min or num > max then
error(("number outside of range (expected %s to be within %s and %s)"):format(num, min, max), 3)
end
return num
end
return setmetatable({
expect = expect,
field = field,
range = range,
}, { __call = function(_, ...) return expect(...) end })

File diff suppressed because it is too large Load diff

122
Capy64/Assets/bios.lua Normal file
View file

@ -0,0 +1,122 @@
local term = require("term")
local timer = require("timer")
local bootSleep = 2000
local bg = 0x0
local fg = 0xffffff
local function sleep(n)
local timerId = timer.start(n)
repeat
local ev, par = coroutine.yield("timer")
until par == timerId
end
local function writeCenter(text)
local w, h = term.getSize()
local _, y = term.getPos()
term.setPos(
(1 + w / 2) - (#text / 2),
y
)
term.write(text)
end
local w, h = term.getSize()
term.setBlink(false)
local function setupScreen()
local options = {
{
"Open data folder",
openDataFolder,
},
{
"Install default OS",
installOS,
},
{
"Exit setup",
exit,
},
{
"Shutdown",
os.shutdown,
}
}
local selection = 1
local function redraw()
term.setForeground(fg)
term.setBackground(bg)
term.clear()
term.setPos(1,2)
writeCenter("Capy64 Setup")
term.setPos(1,3)
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])
end
end
end
while true do
redraw()
local ev = { coroutine.yield("key_down") }
if ev[3] == "up" then
selection = selection - 1
elseif ev[3] == "down" then
selection = selection + 1
elseif ev[3] == "enter" then
options[selection][2]()
elseif ev[3] == "esc" then
exit()
end
if selection > #options then
selection = 1
elseif selection < 1 then
selection = #options
end
end
end
local function bootScreen()
term.setPos(1,2)
writeCenter("Capy64")
term.setPos(1,4)
writeCenter("Powered by Capybaras")
term.setPos(1, h - 1)
writeCenter("Press F2 to open setup")
local timerId = timer.start(bootSleep)
while true do
local ev = {coroutine.yield("timer", "key_down")}
if ev[1] == "timer" and ev[2] == timerId then
exit()
elseif ev[1] == "key_down" and ev[3] == "f2" then
setupScreen()
break
end
end
end
bootScreen()
term.clear()
exit()
while true do
coroutine.yield()
end

BIN
Capy64/Assets/font.ttf Normal file

Binary file not shown.

180
Capy64/BIOS/Bios.cs Normal file
View file

@ -0,0 +1,180 @@
using Capy64.API;
using Capy64.Core;
using Capy64.Eventing;
using Capy64.Eventing.Events;
using Capy64.LuaRuntime;
using Capy64.LuaRuntime.Libraries;
using KeraLua;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Xml.Linq;
using System.Diagnostics;
namespace Capy64.BIOS;
public class Bios : IPlugin
{
private static IGame _game;
private EventEmitter _eventEmitter;
private RuntimeInputEvents _runtimeInputEvents;
private Drawing _drawing;
private bool CloseRuntime = false;
public Bios(IGame game)
{
_game = game;
_eventEmitter = game.EventEmitter;
_drawing = game.Drawing;
_game.EventEmitter.OnInit += OnInit;
_eventEmitter.OnTick += OnTick;
}
private void OnInit(object sender, EventArgs e)
{
RunBIOS();
}
private void OnTick(object sender, TickEvent e)
{
if (CloseRuntime)
{
_runtimeInputEvents.Unregister();
_game.LuaRuntime.Close();
CloseRuntime = false;
StartLuaOS();
}
Resume();
}
private void RunBIOS()
{
_game.LuaRuntime = new();
InitLuaPlugins();
_game.LuaRuntime.Thread.PushCFunction(L_OpenDataFolder);
_game.LuaRuntime.Thread.SetGlobal("openDataFolder");
_game.LuaRuntime.Thread.PushCFunction(L_InstallOS);
_game.LuaRuntime.Thread.SetGlobal("installOS");
_game.LuaRuntime.Thread.PushCFunction(L_Exit);
_game.LuaRuntime.Thread.SetGlobal("exit");
var status = _game.LuaRuntime.Thread.LoadFile("Assets/bios.lua");
if (status != LuaStatus.OK)
{
throw new LuaException(_game.LuaRuntime.Thread.ToString(-1));
}
_runtimeInputEvents = new RuntimeInputEvents(_eventEmitter, _game.LuaRuntime);
_runtimeInputEvents.Register();
}
private void StartLuaOS()
{
InstallOS();
try
{
_game.LuaRuntime = new Runtime();
InitLuaPlugins();
_game.LuaRuntime.Patch();
_game.LuaRuntime.Init();
_runtimeInputEvents = new(_eventEmitter, _game.LuaRuntime);
_runtimeInputEvents.Register();
}
catch (LuaException ex)
{
var panic = new PanicScreen(_game.Drawing);
_drawing.Begin();
panic.Render("Cannot load operating system!", ex.Message);
_drawing.End();
}
}
public void Resume()
{
try
{
var yielded = _game.LuaRuntime.Resume();
if (!yielded)
{
_game.Exit();
}
}
catch (LuaException e)
{
Console.WriteLine(e);
var panic = new PanicScreen(_game.Drawing);
panic.Render(e.Message);
_runtimeInputEvents.Unregister();
}
}
private void InitLuaPlugins()
{
var allPlugins = new List<IPlugin>(_game.NativePlugins);
allPlugins.AddRange(_game.Plugins);
foreach (var plugin in allPlugins)
{
plugin.LuaInit(_game.LuaRuntime.Thread);
}
}
public void InstallOS(bool force = false)
{
var installedFilePath = Path.Combine(FileSystem.BasePath, ".installed");
if (!File.Exists(installedFilePath) || force)
{
FileSystem.CopyDirectory("Assets/Lua", FileSystem.DataPath, true, true);
File.Create(installedFilePath).Dispose();
}
}
public static void Shutdown()
{
_game.Exit();
}
private int L_OpenDataFolder(IntPtr state)
{
var path = FileSystem.DataPath;
switch (Environment.OSVersion.Platform)
{
case PlatformID.Win32NT:
Process.Start("explorer.exe", path);
break;
case PlatformID.Unix:
Process.Start("xdg-open", path);
break;
}
return 0;
}
private int L_InstallOS(IntPtr state)
{
InstallOS(true);
return 0;
}
private int L_Exit(IntPtr state)
{
CloseRuntime = true;
return 0;
}
}

View file

@ -0,0 +1,61 @@
using Capy64.Core;
using Capy64.LuaRuntime.Libraries;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.BIOS;
public class PanicScreen
{
public Color ForegroundColor = Color.White;
public Color BackgroundColor = new Color(0, 51, 187);
private Drawing _drawing;
public PanicScreen(Drawing drawing)
{
_drawing = drawing;
}
public void Render(string error, string details = null)
{
Term.ForegroundColor = ForegroundColor;
Term.BackgroundColor = BackgroundColor;
Term.SetCursorBlink(false);
Term.SetSize(57, 23);
Term.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);
Term.ForegroundColor = ForegroundColor;
Term.BackgroundColor = BackgroundColor;
Term.SetCursorPosition(1, 4);
Print(error + '\n');
if (details is not null) {
Print(details);
}
}
private void Print(string txt)
{
foreach (var ch in txt)
{
Term.Write(ch.ToString());
if(Term.CursorPosition.X >= Term.Width || ch == '\n')
{
Term.SetCursorPosition(1, (int)Term.CursorPosition.Y + 1);
}
}
Term.SetCursorPosition(1, (int)Term.CursorPosition.Y + 1);
}
}

View file

@ -0,0 +1,127 @@
using Capy64.Eventing.Events;
using Capy64.Eventing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Capy64.LuaRuntime;
using Microsoft.Xna.Framework.Input;
namespace Capy64.BIOS;
internal class RuntimeInputEvents
{
private EventEmitter _eventEmitter;
private Runtime _runtime;
public RuntimeInputEvents(EventEmitter eventEmitter, Runtime runtime)
{
_eventEmitter = eventEmitter;
_runtime = runtime;
}
public void Register()
{
_eventEmitter.OnMouseUp += OnMouseUp;
_eventEmitter.OnMouseDown += OnMouseDown;
_eventEmitter.OnMouseMove += OnMouseMove;
_eventEmitter.OnMouseWheel += OnMouseWheel;
_eventEmitter.OnKeyUp += OnKeyUp;
_eventEmitter.OnKeyDown += OnKeyDown;
_eventEmitter.OnChar += OnChar;
}
public void Unregister()
{
_eventEmitter.OnMouseUp -= OnMouseUp;
_eventEmitter.OnMouseDown -= OnMouseDown;
_eventEmitter.OnMouseMove -= OnMouseMove;
_eventEmitter.OnMouseWheel -= OnMouseWheel;
_eventEmitter.OnKeyUp -= OnKeyUp;
_eventEmitter.OnKeyDown -= OnKeyDown;
_eventEmitter.OnChar -= OnChar;
}
private void OnMouseUp(object sender, MouseButtonEvent e)
{
_runtime.PushEvent("mouse_up", new object[]
{
(int)e.Button,
e.Position.X,
e.Position.Y,
});
}
private void OnMouseDown(object sender, MouseButtonEvent e)
{
_runtime.PushEvent("mouse_down", new object[]
{
(int)e.Button,
e.Position.X,
e.Position.Y,
});
}
private void OnMouseMove(object sender, MouseMoveEvent e)
{
_runtime.PushEvent("mouse_move", new object[]
{
e.PressedButtons,
e.Position.X,
e.Position.Y,
});
}
private void OnMouseWheel(object sender, MouseWheelEvent e)
{
_runtime.PushEvent("mouse_scroll", new object[]
{
e.Position.X,
e.Position.Y,
e.VerticalValue,
e.HorizontalValue,
});
}
private void OnKeyUp(object sender, KeyEvent e)
{
_runtime.PushEvent("key_up", new object[]
{
e.KeyCode,
e.KeyName,
(int)e.Mods
});
}
private void OnKeyDown(object sender, KeyEvent e)
{
_runtime.PushEvent("key_down", new object[]
{
e.KeyCode,
e.KeyName,
(int)e.Mods,
e.IsHeld,
});
if (e.Mods.HasFlag(InputManager.Modifiers.LCtrl) || e.Mods.HasFlag(InputManager.Modifiers.RCtrl) && !e.IsHeld)
{
if (e.Key == Keys.C)
{
_runtime.PushEvent(new LuaEvent()
{
Name = "interrupt",
Parameters = { },
BypassFilter = true,
});
}
}
}
private void OnChar(object sender, CharEvent e)
{
_runtime.PushEvent("char", new object[]
{
e.Character,
});
}
}

171
Capy64/Capy64.cs Normal file
View file

@ -0,0 +1,171 @@
using Capy64.API;
using Capy64.Core;
using Capy64.Eventing;
using Capy64.Extensions;
using Capy64.Eventing.Events;
using Capy64.LuaRuntime;
using Capy64.PluginManager;
using KeraLua;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using static Capy64.Utils;
namespace Capy64;
public class Capy64 : Game, IGame
{
public Game Game => this;
public string Version => "Capy64 a0.0.1";
public IList<IPlugin> NativePlugins { get; private set; }
public IList<IPlugin> Plugins { get; private set; }
public int Width { get; set; } = 400;
public int Height { get; set; } = 300;
public float Scale { get; set; } = 2f;
public Drawing Drawing { get; private set; }
public Runtime LuaRuntime { get; set; }
public EventEmitter EventEmitter { get; private set; }
public Borders Borders = new()
{
Top = 0,
Bottom = 0,
Left = 0,
Right = 0,
};
private readonly InputManager _inputManager;
private RenderTarget2D renderTarget;
private readonly GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private IServiceProvider _serviceProvider;
private ulong _totalTicks = 0;
public Capy64()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
EventEmitter = new();
_inputManager = new(this, EventEmitter);
Drawing = new();
}
public void ConfigureServices(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void UpdateSize()
{
_graphics.PreferredBackBufferWidth = (int)(Width * Scale) + Borders.Left + Borders.Right;
_graphics.PreferredBackBufferHeight = (int)(Height * Scale) + Borders.Top + Borders.Right;
_graphics.ApplyChanges();
renderTarget = new RenderTarget2D(
GraphicsDevice,
Width,
Height,
false,
GraphicsDevice.PresentationParameters.BackBufferFormat,
DepthFormat.Depth24, 0, RenderTargetUsage.PreserveContents);
Drawing.Canvas = renderTarget;
_inputManager.Texture = renderTarget;
_inputManager.WindowScale = Scale;
EventEmitter.RaiseScreenSizeChange();
}
private void OnWindowSizeChange(object sender, EventArgs e)
{
var bounds = Window.ClientBounds;
Console.WriteLine(bounds);
if (Window.IsMaximized())
{
}
Width = (int)(bounds.Width / Scale);
Height = (int)(bounds.Height / Scale);
UpdateSize();
}
protected override void Initialize()
{
UpdateSize();
Window.AllowUserResizing = false;
Window.ClientSizeChanged += OnWindowSizeChange;
NativePlugins = GetNativePlugins();
Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider);
EventEmitter.RaiseInit();
base.Initialize();
}
private List<IPlugin> GetNativePlugins()
{
var iType = typeof(IPlugin);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => iType.IsAssignableFrom(p) && !p.IsInterface);
var plugins = new List<IPlugin>();
foreach (var type in types)
{
var instance = (IPlugin)ActivatorUtilities.CreateInstance(_serviceProvider, type)!;
plugins.Add(instance);
}
return plugins;
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void Update(GameTime gameTime)
{
Drawing.Begin();
EventEmitter.RaiseTick(new()
{
GameTime = gameTime,
TotalTicks = _totalTicks
});
// resume here
Drawing.End();
// Register user input
_inputManager.Update(IsActive);
base.Update(gameTime);
_totalTicks++;
}
protected override void Draw(GameTime gameTime)
{
_spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp);
//GraphicsDevice.Clear(Color.Blue);
_spriteBatch.Draw(renderTarget, new(Borders.Left, Borders.Top), null, Color.White, 0f, Vector2.Zero, Scale, SpriteEffects.None, 0);
_spriteBatch.End();
base.Draw(gameTime);
}
}

46
Capy64/Capy64.csproj Normal file
View file

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\font.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Assets\Lua/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icon.ico" />
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FontStashSharp.MonoGame" Version="1.2.8" />
<PackageReference Include="KeraLua" Version="1.3.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.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>
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" />
</Target>
</Project>

View file

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

73
Capy64/Core/Audio.cs Normal file
View file

@ -0,0 +1,73 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using System;
using System.Threading.Channels;
namespace Capy64.Core;
public class Audio
{
public const int SampleRate = 48000;
public const int SamplesPerBuffer = 3000;
const int bytesPerSample = 2;
private readonly DynamicSoundEffectInstance _instance;
private double _time = 0.0;
public Audio()
{
_instance = new(SampleRate, AudioChannels.Mono);
}
public void test()
{
var workingBuffer = new float[SamplesPerBuffer];
FillWorkingBuffer(workingBuffer);
var buffer = DivideBuffer(workingBuffer);
_instance.SubmitBuffer(buffer);
}
private byte[] DivideBuffer(float[] from)
{
var outBuffer = new byte[SamplesPerBuffer * bytesPerSample];
for (int i = 0; i < from.Length; i++) {
var floatSample = MathHelper.Clamp(from[i], -1.0f, 1.0f);
var shortSample = (short)(floatSample * short.MaxValue);
int index = i * bytesPerSample + bytesPerSample;
if (!BitConverter.IsLittleEndian)
{
outBuffer[index] = (byte)(shortSample >> 8);
outBuffer[index + 1] = (byte)shortSample;
}
else
{
outBuffer[index] = (byte)shortSample;
outBuffer[index + 1] = (byte)(shortSample >> 8);
}
}
return outBuffer;
}
public static double SineWave(double time, double frequency)
{
return Math.Sin(time * 2 * Math.PI * frequency);
}
private void FillWorkingBuffer(float[] buffer)
{
for (int i = 0; i < SamplesPerBuffer; i++)
{
// Here is where you sample your wave function
buffer[i] = (float)SineWave(_time, 440); // Left Channel
// Advance time passed since beginning
// Since the amount of samples in a second equals the chosen SampleRate
// Then each sample should advance the time by 1 / SampleRate
_time += 1.0 / SampleRate;
}
}
}

176
Capy64/Core/Drawing.cs Normal file
View file

@ -0,0 +1,176 @@
using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended;
using System;
using System.Collections.Generic;
using System.IO;
namespace Capy64.Core;
public class Drawing : IDisposable
{
private SpriteBatch _spriteBatch;
private GraphicsDevice _graphicsDevice;
private readonly FontSystem _fontSystem;
private Texture2D _whitePixel;
private RenderTarget2D _canvas;
private bool _isDrawing;
public RenderTarget2D Canvas
{
get => _canvas;
set
{
var isDrawing = _isDrawing;
if (isDrawing)
End();
_canvas = value;
_spriteBatch = new SpriteBatch(_canvas.GraphicsDevice);
_graphicsDevice = _canvas.GraphicsDevice;
_whitePixel = new Texture2D(_spriteBatch.GraphicsDevice, 1, 1, mipmap: false, SurfaceFormat.Color);
_whitePixel.SetData(new Color[1] { Color.White });
if (isDrawing)
Begin();
}
}
public Drawing()
{
_fontSystem = new FontSystem();
_fontSystem.AddFont(File.ReadAllBytes(@"Assets/font.ttf"));
}
public void Begin()
{
if (_isDrawing)
return;
_isDrawing = true;
_graphicsDevice.SetRenderTarget(_canvas);
_graphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true, };
_spriteBatch.Begin();
}
public void End()
{
if (!_isDrawing)
return;
_spriteBatch.End();
_graphicsDevice.SetRenderTarget(null);
_isDrawing = false;
}
public void DrawString(Vector2 pos, string text, Color color, int size = 13)
{
var font = _fontSystem.GetFont(size);
_spriteBatch.DrawString(font, text, pos, color, layerDepth: 0);
}
public Vector2 MeasureString(string text, int size = 13)
{
var font = _fontSystem.GetFont(size);
return font.MeasureString(text);
}
public void Plot(Point point, Color color)
{
var grid = new Color[_canvas.Width * _canvas.Height];
_canvas.GetData(grid);
if (point.X < 0 || point.Y < 0) return;
if (point.X >= _canvas.Width || point.Y >= _canvas.Height) return;
grid[point.X + point.Y * _canvas.Width] = color;
_canvas.SetData(grid);
}
public void Plot(IEnumerable<Point> points, Color color)
{
var grid = new Color[_canvas.Width * _canvas.Height];
_canvas.GetData(grid);
foreach (var point in points)
{
if (point.X < 0 || point.Y < 0) continue;
if (point.X >= _canvas.Width || point.Y >= _canvas.Height) continue;
grid[point.X + point.Y * _canvas.Width] = color;
}
_canvas.SetData(grid);
}
public void UnsafePlot(Point point, Color color)
{
var grid = new Color[_canvas.Width * _canvas.Height];
_canvas.GetData(grid);
grid[point.X + point.Y * _canvas.Width] = color;
_canvas.SetData(grid);
}
public void UnsafePlot(IEnumerable<Point> points, Color color)
{
var grid = new Color[_canvas.Width * _canvas.Height];
_canvas.GetData(grid);
foreach (var point in points)
{
grid[point.X + point.Y * _canvas.Width] = color;
}
_canvas.SetData(grid);
}
public void DrawPoint(Vector2 point, Color color, int size = 1)
{
_spriteBatch.DrawPoint(point, color, size);
}
public void DrawCircle(Vector2 pos, int radius, Color color, int thickness = 1)
{
_spriteBatch.DrawCircle(pos, radius, radius * 4, color, thickness);
}
public void DrawLine(Vector2 start, Vector2 end, Color color, float thickness = 1)
{
_spriteBatch.DrawLine(start, end, color, thickness);
}
public void DrawRectangle(Vector2 pos, Size2 size, Color color, int thickness = 1, float rotation = 0f)
{
DrawRectangle(new(pos, size), color, thickness, rotation, 0f);
}
public void DrawPolygon(Vector2 pos, Vector2[] points, Color color, int thickness = 1)
{
_spriteBatch.DrawPolygon(pos, points, color, thickness);
}
public void DrawEllipse(Vector2 pos, Vector2 radius, Color color, int thickness = 1)
{
var sides = (int)Math.Max(radius.X, radius.Y) * 4;
_spriteBatch.DrawEllipse(pos, radius, sides, color, thickness);
}
public void Clear(Color? color = default)
{
Color finalColor = color ?? Color.Black;
_graphicsDevice.Clear(finalColor);
}
public void DrawRectangle(RectangleF rectangle, Color color, float thickness = 1f, float rotation = 0f, float layerDepth = 0f)
{
Vector2 position = new(rectangle.X, rectangle.Y);
Vector2 position2 = new(rectangle.Right - thickness, rectangle.Y);
Vector2 position3 = new(rectangle.X, rectangle.Bottom - thickness);
Vector2 scale = new(rectangle.Width, thickness);
Vector2 scale2 = new(thickness, rectangle.Height);
_spriteBatch.Draw(_whitePixel, position, null, color, rotation, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
_spriteBatch.Draw(_whitePixel, position, null, color, rotation, Vector2.Zero, scale2, SpriteEffects.None, layerDepth);
_spriteBatch.Draw(_whitePixel, position2, null, color, rotation, Vector2.Zero, scale2, SpriteEffects.None, layerDepth);
_spriteBatch.Draw(_whitePixel, position3, null, color, rotation, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
}
public void Dispose()
{
_spriteBatch.Dispose();
_whitePixel.Dispose();
}
}

View file

@ -0,0 +1,109 @@
using Capy64.Eventing.Events;
using Microsoft.Xna.Framework.Input;
using System;
namespace Capy64.Eventing;
public class EventEmitter
{
// Mouse events
public event EventHandler<MouseButtonEvent> OnMouseDown;
public event EventHandler<MouseButtonEvent> OnMouseUp;
public event EventHandler<MouseMoveEvent> OnMouseMove;
public event EventHandler<MouseWheelEvent> OnMouseWheel;
// Keyboard events
public event EventHandler<KeyEvent> OnKeyDown;
public event EventHandler<KeyEvent> OnKeyUp;
public event EventHandler<CharEvent> OnChar;
// Functional events
public event EventHandler<TickEvent> OnTick;
public event EventHandler OnInit;
public event EventHandler OnScreenSizeChange;
public void RaiseMouseMove(MouseMoveEvent ev)
{
if (OnMouseMove is not null)
{
OnMouseMove(this, ev);
}
}
public void RaiseMouseButton(MouseButtonEvent ev)
{
if (ev.State == ButtonState.Released)
{
if (OnMouseUp is not null)
{
OnMouseUp(this, ev);
}
}
else
{
if (OnMouseDown is not null)
{
OnMouseDown(this, ev);
}
}
}
public void RaiseMouseWheelEvent(MouseWheelEvent ev)
{
if (OnMouseWheel is not null)
{
OnMouseWheel(this, ev);
}
}
public void RaiseKeyDown(KeyEvent ev)
{
if (OnKeyDown is not null)
{
OnKeyDown(this, ev);
}
}
public void RaiseKeyUp(KeyEvent ev)
{
if (OnKeyUp is not null)
{
OnKeyUp(this, ev);
}
}
public void RaiseCharEvent(CharEvent ev)
{
if (OnChar is not null)
{
OnChar(this, ev);
}
}
public void RaiseTick(TickEvent ev)
{
if(OnTick is not null)
{
OnTick(this, ev);
}
}
public void RaiseInit()
{
if(OnInit is not null)
{
OnInit(this, EventArgs.Empty);
}
}
public void RaiseScreenSizeChange()
{
if(OnScreenSizeChange is not null)
{
OnScreenSizeChange(this, EventArgs.Empty);
}
}
}

View file

@ -0,0 +1,8 @@
using System;
namespace Capy64.Eventing.Events;
public class CharEvent : EventArgs
{
public char Character { get; set; }
}

View file

@ -0,0 +1,14 @@
using Microsoft.Xna.Framework.Input;
using System;
using static Capy64.Eventing.InputManager;
namespace Capy64.Eventing.Events;
public class KeyEvent : EventArgs
{
public int KeyCode { get; set; }
public string KeyName { get; set; }
public bool IsHeld { get; set; }
public Keys Key { get; set; }
public Modifiers Mods { get; set; }
}

View file

@ -0,0 +1,13 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System;
using static Capy64.Eventing.InputManager;
namespace Capy64.Eventing.Events;
public class MouseButtonEvent : EventArgs
{
public MouseButton Button { get; set; }
public ButtonState State { get; set; }
public Point Position { get; set; }
}

View file

@ -0,0 +1,10 @@
using Microsoft.Xna.Framework;
using System;
namespace Capy64.Eventing.Events;
public class MouseMoveEvent : EventArgs
{
public Point Position { get; set; }
public int[] PressedButtons { get; set; }
}

View file

@ -0,0 +1,11 @@
using Microsoft.Xna.Framework;
using System;
namespace Capy64.Eventing.Events;
public class MouseWheelEvent : EventArgs
{
public Point Position { get; set; }
public int VerticalValue { get; set; }
public int HorizontalValue { get; set; }
}

View file

@ -0,0 +1,14 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.Eventing.Events;
public class TickEvent : EventArgs
{
public GameTime GameTime { get; set; }
public ulong TotalTicks { get; set; }
}

View file

@ -0,0 +1,282 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Capy64.Eventing;
public class InputManager
{
public enum MouseButton
{
Left = 1,
Right = 2,
Middle = 3,
Button4 = 4,
Button5 = 5,
}
[Flags]
public enum Modifiers
{
LShift = 1,
RShift = 2,
LAlt = 4,
RAlt = 8,
LCtrl = 16,
RCtrl = 32,
}
private static Keys[] IgnoredTextInputKeys =
{
Keys.Enter,
Keys.Back,
Keys.Tab,
Keys.Delete,
Keys.Escape,
Keys.VolumeDown,
Keys.VolumeUp,
Keys.VolumeMute,
};
private readonly Dictionary<MouseButton, ButtonState> mouseButtonStates = new()
{
[MouseButton.Left] = ButtonState.Released,
[MouseButton.Right] = ButtonState.Released,
[MouseButton.Middle] = ButtonState.Released,
[MouseButton.Button4] = ButtonState.Released,
[MouseButton.Button5] = ButtonState.Released,
};
public Texture2D Texture { get; set; }
public float WindowScale { get; set; }
public const int MouseScrollDelta = 120;
private Point mousePosition;
private int vMouseScroll;
private int hMouseScroll;
private Modifiers keyboardMods = 0;
private HashSet<Keys> pressedKeys = new();
private readonly Game _game;
private readonly EventEmitter _eventEmitter;
public InputManager(Game game, EventEmitter eventManager)
{
_game = game;
_eventEmitter = eventManager;
_game.Window.KeyDown += OnKeyDown;
_game.Window.KeyUp += OnKeyUp;
_game.Window.TextInput += OnTextInput;
var mouseState = Mouse.GetState();
vMouseScroll = mouseState.ScrollWheelValue;
hMouseScroll = mouseState.HorizontalScrollWheelValue;
}
public void Update(bool IsActive)
{
UpdateMouse(Mouse.GetState(), IsActive);
UpdateKeyboard(Keyboard.GetState(), IsActive);
}
private void UpdateMouse(MouseState state, bool isActive)
{
if (!isActive)
return;
var rawPosition = state.Position;
var pos = new Point((int)(rawPosition.X / WindowScale), (int)(rawPosition.Y / WindowScale));
if (pos.X < 0 || pos.Y < 0 || pos.X >= Texture.Width || pos.Y >= Texture.Height)
return;
if (pos != mousePosition)
{
_eventEmitter.RaiseMouseMove(new()
{
Position = pos,
PressedButtons = mouseButtonStates.Where(q => q.Value == ButtonState.Pressed).Select(q => (int)q.Key).ToArray()
});
mousePosition = pos;
}
int vValue = 0;
int hValue = 0;
if (state.ScrollWheelValue != vMouseScroll)
{
var vDelta = vMouseScroll - state.ScrollWheelValue;
vMouseScroll = state.ScrollWheelValue;
vValue = vDelta / MouseScrollDelta;
}
if (state.HorizontalScrollWheelValue != hMouseScroll)
{
var hDelta = hMouseScroll - state.ScrollWheelValue;
hMouseScroll = state.ScrollWheelValue;
hValue = hDelta / MouseScrollDelta;
}
if (vValue != 0 || hValue != 0)
{
_eventEmitter.RaiseMouseWheelEvent(new()
{
Position = mousePosition,
VerticalValue = vValue,
HorizontalValue = hValue,
});
}
// detect changes in mouse buttons
if (state.LeftButton != mouseButtonStates[MouseButton.Left])
{
_eventEmitter.RaiseMouseButton(new()
{
Button = MouseButton.Left,
State = state.LeftButton,
Position = mousePosition,
});
}
if (state.RightButton != mouseButtonStates[MouseButton.Right])
{
_eventEmitter.RaiseMouseButton(new()
{
Button = MouseButton.Right,
State = state.RightButton,
Position = mousePosition,
});
}
if (state.MiddleButton != mouseButtonStates[MouseButton.Middle])
{
_eventEmitter.RaiseMouseButton(new()
{
Button = MouseButton.Middle,
State = state.MiddleButton,
Position = mousePosition,
});
}
if (state.XButton1 != mouseButtonStates[MouseButton.Button4])
{
_eventEmitter.RaiseMouseButton(new()
{
Button = MouseButton.Button4,
State = state.XButton1,
Position = mousePosition,
});
}
if (state.XButton2 != mouseButtonStates[MouseButton.Button5])
{
_eventEmitter.RaiseMouseButton(new()
{
Button = MouseButton.Button5,
State = state.XButton2,
Position = mousePosition,
});
}
// update mouse states
mouseButtonStates[MouseButton.Left] = state.LeftButton;
mouseButtonStates[MouseButton.Right] = state.RightButton;
mouseButtonStates[MouseButton.Middle] = state.MiddleButton;
mouseButtonStates[MouseButton.Button4] = state.XButton1;
mouseButtonStates[MouseButton.Button5] = state.XButton2;
}
private void UpdateKeyboard(KeyboardState state, bool isActive)
{
var keys = state.GetPressedKeys();
if (keys.Contains(Keys.LeftControl))
keyboardMods |= Modifiers.LCtrl;
else
keyboardMods &= ~Modifiers.LCtrl;
if (keys.Contains(Keys.RightControl))
keyboardMods |= Modifiers.RCtrl;
else
keyboardMods &= ~Modifiers.RCtrl;
if (keys.Contains(Keys.LeftAlt))
keyboardMods |= Modifiers.LAlt;
else
keyboardMods &= ~Modifiers.LAlt;
if (keys.Contains(Keys.RightAlt))
keyboardMods |= Modifiers.RAlt;
else
keyboardMods &= ~Modifiers.RAlt;
if (keys.Contains(Keys.LeftShift))
keyboardMods |= Modifiers.LShift;
else
keyboardMods &= ~Modifiers.LShift;
if (keys.Contains(Keys.RightShift))
keyboardMods |= Modifiers.RShift;
else
keyboardMods &= ~Modifiers.RShift;
}
private void OnKeyDown(object sender, InputKeyEventArgs e)
{
_eventEmitter.RaiseKeyDown(new()
{
KeyCode = (int)e.Key,
KeyName = GetKeyName(e.Key),
IsHeld = pressedKeys.Contains(e.Key),
Key = e.Key,
Mods = keyboardMods,
});
pressedKeys.Add(e.Key);
}
private void OnKeyUp(object sender, InputKeyEventArgs e)
{
_eventEmitter.RaiseKeyUp(new()
{
KeyCode = (int)e.Key,
KeyName = GetKeyName(e.Key),
Key = e.Key,
Mods = keyboardMods,
});
pressedKeys.Remove(e.Key);
}
private void OnTextInput(object sender, TextInputEventArgs e)
{
if (IgnoredTextInputKeys.Contains(e.Key))
{
return;
}
_eventEmitter.RaiseCharEvent(new()
{
Character = e.Character,
});
}
public static string GetKeyName(Keys key)
{
var keyName = ToUnderscore(key.ToString());
if (key >= Keys.D0 && key <= Keys.D9)
{
keyName = keyName.TrimStart('d');
}
keyName = keyName.Replace("oem_", "");
return keyName;
}
public static string ToUnderscore(string str)
{
return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.Extensions.Bindings;
public partial class SDL2
{
private const string SDL = "SDL2.dll";
[LibraryImport(SDL)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static partial void SDL_MaximizeWindow(IntPtr window);
[LibraryImport(SDL)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static partial uint SDL_GetWindowFlags(IntPtr window);
}

View file

@ -0,0 +1,62 @@
using Capy64.Extensions.Bindings;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.Extensions;
public static class GameWindowExtensions
{
/// <summary>
/// <see cref="https://github.com/flibitijibibo/SDL2-CS/blob/master/src/SDL2.cs#L1530"/>
/// </summary>
[Flags]
public enum WindowFlags : uint
{
SDL_WINDOW_FULLSCREEN = 0x00000001,
SDL_WINDOW_OPENGL = 0x00000002,
SDL_WINDOW_SHOWN = 0x00000004,
SDL_WINDOW_HIDDEN = 0x00000008,
SDL_WINDOW_BORDERLESS = 0x00000010,
SDL_WINDOW_RESIZABLE = 0x00000020,
SDL_WINDOW_MINIMIZED = 0x00000040,
SDL_WINDOW_MAXIMIZED = 0x00000080,
SDL_WINDOW_MOUSE_GRABBED = 0x00000100,
SDL_WINDOW_INPUT_FOCUS = 0x00000200,
SDL_WINDOW_MOUSE_FOCUS = 0x00000400,
SDL_WINDOW_FULLSCREEN_DESKTOP =
(SDL_WINDOW_FULLSCREEN | 0x00001000),
SDL_WINDOW_FOREIGN = 0x00000800,
SDL_WINDOW_ALLOW_HIGHDPI = 0x00002000,
SDL_WINDOW_MOUSE_CAPTURE = 0x00004000,
SDL_WINDOW_ALWAYS_ON_TOP = 0x00008000,
SDL_WINDOW_SKIP_TASKBAR = 0x00010000,
SDL_WINDOW_UTILITY = 0x00020000,
SDL_WINDOW_TOOLTIP = 0x00040000,
SDL_WINDOW_POPUP_MENU = 0x00080000,
SDL_WINDOW_KEYBOARD_GRABBED = 0x00100000,
SDL_WINDOW_VULKAN = 0x10000000,
SDL_WINDOW_METAL = 0x2000000,
SDL_WINDOW_INPUT_GRABBED =
SDL_WINDOW_MOUSE_GRABBED,
}
public static WindowFlags GetWindowFlags(this GameWindow window)
{
return (WindowFlags)SDL2.SDL_GetWindowFlags(window.Handle);
}
public static void MaximizeWindow(this GameWindow window)
{
SDL2.SDL_MaximizeWindow(window.Handle);
}
public static bool IsMaximized(this GameWindow window)
{
return window.GetWindowFlags().HasFlag(WindowFlags.SDL_WINDOW_MAXIMIZED);
}
}

31
Capy64/IGame.cs Normal file
View file

@ -0,0 +1,31 @@
using Capy64.API;
using Capy64.Core;
using Capy64.Eventing;
using Capy64.LuaRuntime;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Capy64;
public interface IGame
{
Game Game { get; }
public string Version { get; }
IList<IPlugin> NativePlugins { get; }
IList<IPlugin> Plugins { get; }
GameWindow Window { get; }
Drawing Drawing { get; }
Runtime LuaRuntime { get; set; }
EventEmitter EventEmitter { get; }
void ConfigureServices(IServiceProvider serviceProvider);
int Width { get; set; }
int Height { get; set; }
float Scale { get; set; }
void UpdateSize();
event EventHandler<EventArgs> Exiting;
void Run();
void Exit();
}

BIN
Capy64/Icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
Capy64/Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime;
public class Constants
{
public const int MULTRET = -1;
}

View file

@ -0,0 +1,57 @@
using KeraLua;
namespace Capy64.LuaRuntime.Extensions
{
static class LuaExtensions
{
public static void OpenBase(this Lua lua)
{
lua.RequireF("_G", NativeLibraries.luaopen_base, true);
}
public static void OpenCoroutine(this Lua lua)
{
lua.RequireF("coroutine", NativeLibraries.luaopen_coroutine, true);
}
public static void OpenDebug(this Lua lua)
{
lua.RequireF("debug", NativeLibraries.luaopen_debug, true);
}
public static void OpenIO(this Lua lua)
{
lua.RequireF("io", NativeLibraries.luaopen_io, true);
}
public static void OpenMath(this Lua lua)
{
lua.RequireF("math", NativeLibraries.luaopen_math, true);
}
public static void OpenOS(this Lua lua)
{
lua.RequireF("os", NativeLibraries.luaopen_os, true);
}
public static void OpenPackage(this Lua lua)
{
lua.RequireF("package", NativeLibraries.luaopen_package, true);
}
public static void OpenString(this Lua lua)
{
lua.RequireF("string", NativeLibraries.luaopen_string, true);
}
public static void OpenTable(this Lua lua)
{
lua.RequireF("table", NativeLibraries.luaopen_table, true);
}
public static void OpenUTF8(this Lua lua)
{
lua.RequireF("utf8", NativeLibraries.luaopen_utf8, true);
}
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Runtime.InteropServices;
namespace Capy64.LuaRuntime.Extensions
{
internal static partial class NativeLibraries
{
#if __IOS__ || __TVOS__ || __WATCHOS__ || __MACCATALYST__
private const string LuaLibraryName = "@rpath/liblua54.framework/liblua54";
#elif __ANDROID__
private const string LuaLibraryName = "liblua54.so";
#elif __MACOS__
private const string LuaLibraryName = "liblua54.dylib";
#elif WINDOWS_UWP
private const string LuaLibraryName = "lua54.dll";
#else
private const string LuaLibraryName = "lua54";
#endif
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_base(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_coroutine(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_debug(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_io(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_math(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_os(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_package(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_string(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_table(IntPtr luaState);
[LibraryImport(LuaLibraryName)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
internal static partial int luaopen_utf8(IntPtr luaState);
}
}

View file

@ -0,0 +1,102 @@
using KeraLua;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Extensions;
public static class Utils
{
public static void PushArray(this Lua state, object obj)
{
var iterable = obj as IEnumerable;
state.NewTable();
long i = 1;
foreach (var item in iterable)
{
state.PushValue(item);
state.RawSetInteger(-2, i++);
}
state.SetTop(-1);
}
public static int PushValue(this Lua state, object? obj)
{
var type = obj.GetType();
switch (obj)
{
case string str:
state.PushString(str);
break;
case char:
state.PushString(obj.ToString());
break;
case byte:
case sbyte:
case short:
case ushort:
case int:
case uint:
case double:
state.PushNumber(Convert.ToDouble(obj));
break;
case long l:
state.PushInteger(l);
break;
case bool b:
state.PushBoolean(b);
break;
case null:
state.PushNil();
break;
case byte[] b:
state.PushBuffer(b);
break;
case LuaFunction func:
state.PushCFunction(func);
break;
case IntPtr ptr:
state.PushLightUserData(ptr);
break;
default:
if (type.IsArray)
{
state.PushArray(obj);
}
else
{
throw new Exception("Invalid type provided");
}
break;
}
return 1;
}
public static void PushManagedObject<T>(this Lua state, T obj)
{
var type = obj.GetType();
var members = type.GetMembers().Where(m => m.MemberType == MemberTypes.Method);
state.CreateTable(0, members.Count());
foreach (var m in members)
{
state.PushCFunction(L => (int)type.InvokeMember(m.Name, BindingFlags.InvokeMethod, null, obj, new object[] { L }));
state.SetField(-2, m.Name);
}
}
}

View file

@ -0,0 +1,740 @@
using Capy64.API;
using Capy64.LuaRuntime.Extensions;
using KeraLua;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
namespace Capy64.LuaRuntime.Libraries;
public class FileSystem : IPlugin
{
public static string BasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "Capy64");
public static string DataPath = Path.Combine(BasePath, "data");
public FileSystem()
{
if (!Directory.Exists(DataPath))
{
Directory.CreateDirectory(DataPath);
}
}
// functions to add to the library, always end libraries with null
private LuaRegister[] FsLib = new LuaRegister[] {
new()
{
name = "list",
function = L_List,
},
new()
{
name = "combine",
function = L_Combine,
},
new()
{
name = "getName",
function = L_GetPathName,
},
new()
{
name = "getDir",
function = L_GetDirectoryName,
},
new()
{
name = "getSize",
function = L_GetFileSize,
},
new()
{
name = "exists",
function = L_Exists,
},
new()
{
name = "isReadOnly",
function = L_IsReadOnly,
},
new()
{
name = "makeDir",
function = L_MakeDir,
},
new()
{
name = "move",
function = L_Move,
},
new()
{
name = "copy",
function = L_Copy,
},
new()
{
name = "delete",
function = L_Delete,
},
new()
{
name = "getAttributes",
function = L_GetAttributes,
},
new()
{
name = "open",
function = L_Open,
},
new(), // NULL
};
public void LuaInit(Lua state)
{
// Add "fs" library to lua, not global (uses require())
state.RequireF("fs", Open, false);
}
private int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(FsLib);
return 1;
}
public static string SanitizePath(string path)
{
// Replace \ to / for cross compatibility in case users prefer to use \
path = path.Replace("\\", "/");
// Get drive root (C:\ for Windows, / for *nix)
var rootPath = Path.GetFullPath(Path.GetPathRoot("/") ?? "/");
// Join path to rootPath and resolves to absolute path
// Relative paths are resolved here (es. ../ and ./)
var absolutePath = Path.GetFullPath(path, rootPath);
// Trim root from path
return absolutePath.Remove(0, rootPath.Length);
}
public static string Resolve(string path)
{
var isolatedPath = SanitizePath(path);
// Now join the isolatedPath to the Lua directory, always inside of it
var resolvedPath = Path.Join(DataPath, isolatedPath);
return resolvedPath;
}
public static string CleanOutputPath(string path)
{
if (string.IsNullOrEmpty(path))
return "";
var clean = path.Replace(Path.DirectorySeparatorChar, '/');
return clean;
}
public static string TrimBasePath(string path)
{
return path;
}
public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive, bool overwrite = false)
{
var dir = new DirectoryInfo(sourceDir);
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
DirectoryInfo[] dirs = dir.GetDirectories();
Directory.CreateDirectory(destinationDir);
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
file.CopyTo(targetFilePath, overwrite);
}
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true, overwrite);
}
}
}
private static int L_List(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = L.CheckString(1);
path = Resolve(path);
if (!Directory.Exists(path))
{
L.Error("directory not found");
}
var fileList = Directory.EnumerateFileSystemEntries(path);
var list = new List<string>();
foreach (var file in fileList)
{
var sfile = Path.GetFileName(file);
list.Add(CleanOutputPath(sfile));
}
L.PushArray(list.Order().ToArray());
return 1;
}
private static int L_Combine(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var nargs = L.GetTop();
var parts = new List<string>();
for (int i = 1; i <= nargs; i++)
{
var pathPart = L.CheckString(i);
parts.Add(pathPart);
}
var result = Path.Join(parts.ToArray());
if (string.IsNullOrEmpty(result))
{
L.PushString("");
return 1;
}
result = SanitizePath(result);
L.PushString(CleanOutputPath(result));
return 1;
}
private static int L_GetPathName(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = L.CheckString(1);
var result = Path.GetFileName(path);
L.PushString(CleanOutputPath(result));
return 1;
}
private static int L_GetDirectoryName(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = L.CheckString(1);
var result = Path.GetDirectoryName(path);
L.PushString(CleanOutputPath(result));
return 1;
}
private static int L_GetFileSize(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = L.CheckString(1);
path = Resolve(path);
if (!File.Exists(path))
{
L.Error("file not found");
}
var fileInfo = new FileInfo(path);
L.PushInteger(fileInfo.Length);
return 1;
}
private static int L_Exists(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = L.CheckString(1);
var exists = Path.Exists(Resolve(path));
L.PushBoolean(exists);
return 1;
}
private static int L_IsReadOnly(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = Resolve(L.CheckString(1));
if (File.Exists(path))
{
var fileInfo = new FileInfo(path);
L.PushBoolean(fileInfo.IsReadOnly);
}
else if (Directory.Exists(path))
{
var dirInfo = new DirectoryInfo(path);
var isReadOnly = dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly);
L.PushBoolean(isReadOnly);
}
else
{
L.Error("path not found");
}
return 1;
}
private static int L_MakeDir(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = Resolve(L.CheckString(1));
if (Path.Exists(path))
{
L.Error("path already exists");
}
Directory.CreateDirectory(path);
return 0;
}
private static int L_Move(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var sourcePath = Resolve(L.CheckString(1));
var destPath = Resolve(L.CheckString(2));
if (!Path.Exists(sourcePath))
{
L.Error("source path not found");
}
if (Path.Exists(destPath))
{
L.Error("destination path already exists");
}
var attr = File.GetAttributes(sourcePath);
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Move(sourcePath, destPath);
}
else
{
File.Move(sourcePath, destPath);
}
return 0;
}
private static int L_Copy(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var sourcePath = Resolve(L.CheckString(1));
var destPath = Resolve(L.CheckString(2));
if (!Path.Exists(sourcePath))
{
L.Error("source path not found");
}
if (Path.Exists(destPath))
{
L.Error("destination path already exists");
}
var attr = File.GetAttributes(sourcePath);
if (attr.HasFlag(FileAttributes.Directory))
{
CopyDirectory(sourcePath, destPath, true);
}
else
{
File.Copy(sourcePath, destPath);
}
return 0;
}
private static int L_Delete(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = Resolve(L.CheckString(1));
bool recursive = false;
if (!L.IsNoneOrNil(2))
{
L.CheckType(2, LuaType.Boolean);
recursive = L.ToBoolean(2);
}
if (!Path.Exists(path))
{
L.Error("path not found");
}
var attr = File.GetAttributes(path);
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Delete(path, recursive);
}
else
{
File.Delete(path);
}
return 0;
}
private static int L_GetAttributes(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = Resolve(L.CheckString(1));
// { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number }
if (!Path.Exists(path))
{
L.Error("path not found");
}
var attributes = new Dictionary<string, object>();
var pathAttributes = File.GetAttributes(path);
if (pathAttributes.HasFlag(FileAttributes.Directory))
{
var dattrs = new DirectoryInfo(path);
attributes["size"] = 0;
attributes["isDirectory"] = true;
attributes["isReadOnly"] = dattrs.Attributes.HasFlag(FileAttributes.ReadOnly);
attributes["created"] = new DateTimeOffset(dattrs.CreationTimeUtc).ToUnixTimeMilliseconds();
attributes["modified"] = new DateTimeOffset(dattrs.LastWriteTimeUtc).ToUnixTimeMilliseconds();
}
else
{
var fattrs = new FileInfo(path);
attributes["size"] = fattrs.Length;
attributes["isDirectory"] = false;
attributes["isReadOnly"] = fattrs.IsReadOnly;
attributes["created"] = new DateTimeOffset(fattrs.CreationTimeUtc).ToUnixTimeMilliseconds();
attributes["modified"] = new DateTimeOffset(fattrs.LastWriteTimeUtc).ToUnixTimeMilliseconds();
}
L.NewTable();
foreach (var attribute in attributes)
{
L.PushString(attribute.Key);
L.PushValue(attribute.Value);
L.SetTable(-3);
}
return 1;
}
private static int L_Open(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var path = Resolve(L.CheckString(1));
var mode = L.CheckString(2);
var errorMessage = "invalid file mode";
if (mode.Length < 1 && mode.Length > 2)
{
L.ArgumentError(2, errorMessage);
return 0;
}
FileMode fileMode;
FileAccess fileAccess;
switch (mode[0])
{
case 'r':
if (!File.Exists(path))
{
L.Error("file not found");
return 0;
}
fileMode = FileMode.Open;
fileAccess = FileAccess.Read;
break;
case 'w':
fileMode = FileMode.CreateNew;
fileAccess = FileAccess.Write;
break;
case 'a':
fileMode = FileMode.Append;
fileAccess = FileAccess.Write;
break;
default:
L.ArgumentError(2, errorMessage);
return 0;
}
bool binaryMode = false;
if (mode.Length == 2)
{
if (mode[1] == 'b')
{
binaryMode = true;
}
else
{
L.ArgumentError(2, errorMessage);
return 0;
}
}
var handle = File.Open(path, fileMode, fileAccess, FileShare.ReadWrite);
bool isClosed = false;
var functions = new Dictionary<string, Func<nint, int>>();
if (fileAccess == FileAccess.Read)
{
var reader = new StreamReader(handle);
functions["read"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
var count = (int)L.OptNumber(1, 1);
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (reader.EndOfStream)
{
L.PushNil();
return 1;
}
if (binaryMode)
{
var data = new byte[count];
handle.Read(data, 0, count);
L.PushBuffer(data);
}
else
{
var data = new char[count];
reader.ReadBlock(data, 0, count);
L.PushString(new string(data));
}
return 1;
};
functions["readLine"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
var count = (int)L.OptNumber(1, 1);
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (reader.EndOfStream)
{
L.PushNil();
return 1;
}
if (binaryMode)
{
var line = reader.ReadLine();
L.PushBuffer(Encoding.UTF8.GetBytes(line));
}
else
{
var line = reader.ReadLine();
L.PushString(line);
}
return 1;
};
functions["readAll"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
var count = (int)L.OptNumber(1, 1);
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (reader.EndOfStream)
{
L.PushNil();
return 1;
}
if (binaryMode)
{
var content = reader.ReadToEnd();
L.PushBuffer(Encoding.UTF8.GetBytes(content));
}
else
{
var content = reader.ReadToEnd();
L.PushString(content);
}
return 1;
};
}
else
{
var writer = new StreamWriter(handle);
functions["write"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
L.ArgumentCheck(L.IsStringOrNumber(1), 1, "string or number value expected");
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (L.IsString(1))
{
if (binaryMode)
{
handle.Write(L.ToBuffer(1));
}
else
{
writer.Write(L.ToString(1));
}
}
else
{
handle.WriteByte((byte)L.ToInteger(1));
}
return 0;
};
functions["writeLine"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
L.ArgumentCheck(L.IsStringOrNumber(1), 1, "string or number value expected");
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (L.IsString(1))
{
if (binaryMode)
{
handle.Write(L.ToBuffer(1));
handle.WriteByte((byte)'\n');
}
else
{
writer.WriteLine(L.ToString(1));
}
}
else
{
handle.WriteByte((byte)L.ToInteger(1));
handle.WriteByte((byte)'\n');
}
return 0;
};
functions["flush"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
handle.Flush();
return 0;
};
}
functions["close"] = (IntPtr state) =>
{
var L = Lua.FromIntPtr(state);
if (isClosed)
{
L.Error("file handle is closed");
return 0;
}
if (fileAccess != FileAccess.Read)
{
handle.Flush();
}
handle.Dispose();
isClosed = true;
return 0;
};
L.NewTable();
foreach (var pair in functions)
{
L.PushString(pair.Key);
L.PushCFunction(new LuaFunction(pair.Value));
L.SetTable(-3);
}
return 1;
}
}

View file

@ -0,0 +1,93 @@
using Capy64.API;
using Capy64.Core;
using KeraLua;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Capy64.LuaRuntime.Libraries;
public class Graphics : IPlugin
{
private static IGame _game;
public Graphics(IGame game)
{
_game = game;
}
private LuaRegister[] GfxLib = new LuaRegister[] {
new()
{
name = "drawPoint",
function = L_DrawPoint,
},
new()
{
name = "drawPoints",
function = L_DrawPoints,
},
new(), // NULL
};
public void LuaInit(Lua state)
{
state.RequireF("graphics", Open, false);
}
public int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(GfxLib);
return 1;
}
private static int L_DrawPoint(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var x = L.CheckInteger(1) - 1;
var y = L.CheckInteger(2) - 1;
var c = L.CheckInteger(3);
var s = L.OptInteger(4, 1);
Utils.UnpackRGB((uint)c, out var r, out var g, out var b);
_game.Drawing.DrawPoint(new(x, y), new Color(r, g, b), (int)s);
return 0;
}
private static int L_DrawPoints(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.CheckType(1, LuaType.Table);
var c = L.CheckInteger(2);
L.SetTop(1);
int size = (int)L.Length(1);
L.ArgumentCheck(size % 2 == 0, 1, "expected an even table");
List<Point> pts = new();
for (int i = 1; i <= size; i += 2)
{
L.GetInteger(1, i);
L.ArgumentCheck(L.IsNumber(-1), 1, "expected number at index " + i);
var x = (int)L.ToNumber(-1) - 1;
L.Pop(1);
L.GetInteger(1, i + 1);
L.ArgumentCheck(L.IsNumber(-1), 1, "expected number at index " + (i + 1));
var y = (int)L.ToNumber(-1) - 1;
L.Pop(1);
pts.Add(new Point(x, y));
}
Utils.UnpackRGB((uint)c, out var r, out var g, out var b);
_game.Drawing.Plot(pts, new(r, g, b));
return 0;
}
}

View file

@ -0,0 +1,22 @@
using Capy64.API;
using KeraLua;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Libraries;
public class HTTP : IPlugin
{
private static IGame _game;
public HTTP(IGame game)
{
_game = game;
}
public void LuaInit(Lua state)
{
}
}

View file

@ -0,0 +1,34 @@
using Capy64.API;
using KeraLua;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Libraries;
public class OS : IPlugin
{
private static IGame _game;
public OS(IGame game)
{
_game = game;
}
public void LuaInit(Lua state)
{
state.GetGlobal("os");
state.PushString("shutdown");
state.PushCFunction(L_Shutdown);
state.SetTable(-3);
}
private static int L_Shutdown(IntPtr state)
{
BIOS.Bios.Shutdown();
return 0;
}
}

View file

@ -0,0 +1,104 @@
using Capy64.API;
using Capy64.Core;
using KeraLua;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Libraries;
public class Screen : IPlugin
{
private static IGame _game;
public Screen(IGame game)
{
_game = game;
}
private LuaRegister[] ScreenLib = new LuaRegister[] {
new()
{
name = "getSize",
function = L_GetSize,
},
new()
{
name = "setSize",
function = L_SetSize,
},
new()
{
name = "getScale",
function = L_GetScale,
},
new()
{
name = "setScale",
function = L_SetScale,
},
new(), // NULL
};
public void LuaInit(Lua state)
{
state.RequireF("screen", Open, false);
}
public int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(ScreenLib);
return 1;
}
private static int L_GetSize(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushInteger(_game.Width);
L.PushInteger(_game.Height);
return 2;
}
private static int L_SetSize(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var w = L.CheckInteger(1);
var h = L.CheckInteger(2);
_game.Width = (int)w;
_game.Height = (int)h;
_game.UpdateSize();
return 0;
}
private static int L_GetScale(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushNumber(_game.Scale);
return 1;
}
private static int L_SetScale(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var s = L.CheckNumber(1);
_game.Scale = (float)s;
_game.UpdateSize();
return 0;
}
}

View file

@ -0,0 +1,574 @@
using Capy64.API;
using Capy64.Eventing.Events;
using KeraLua;
using Microsoft.Xna.Framework;
using System;
namespace Capy64.LuaRuntime.Libraries;
internal class Term : IPlugin
{
private struct Char
{
public char Character;
public Color Foreground;
public Color Background;
}
public const int CharWidth = 7;
public const int CharHeight = 13;
public const int CursorDelay = 30;
public static int Width { get; private set; } = 57;
public static int Height { get; private set; } = 23;
public static int RealWidth => CharWidth * Width;
public static int RealHeight => CharHeight * Height;
public static Vector2 CharOffset => new(1, 0);
public static Vector2 CursorPosition => _cursorPosition + Vector2.One;
private static Vector2 _cursorPosition { get; set; }
public static Color ForegroundColor { get; set; }
public static Color BackgroundColor { get; set; }
private static Char?[] CharGrid;
private static IGame _game;
private static bool cursorState = false;
private static bool enableCursor = true;
public Term(IGame game)
{
_game = game;
_cursorPosition = Vector2.Zero;
ForegroundColor = Color.White;
BackgroundColor = Color.Black;
UpdateSize();
_game.EventEmitter.OnTick += OnTick;
}
private LuaRegister[] TermLib = new LuaRegister[]
{
new()
{
name = "write",
function = L_Write,
},
new()
{
name = "getPos",
function = L_GetPos,
},
new()
{
name = "setPos",
function = L_SetPos,
},
new()
{
name = "getSize",
function = L_GetSize,
},
new()
{
name = "setSize",
function = L_SetSize,
},
new()
{
name = "getForeground",
function = L_GetForegroundColor,
},
new()
{
name = "setForeground",
function = L_SetForegroundColor,
},
new()
{
name = "getBackground",
function = L_GetBackgroundColor,
},
new()
{
name = "setBackground",
function = L_SetBackgroundColor,
},
new()
{
name = "toRealPos",
function = L_ToReal,
},
new()
{
name = "fromRealPos",
function = L_FromReal,
},
new()
{
name = "clear",
function = L_Clear,
},
new()
{
name = "clearLine",
function = L_ClearLine,
},
new()
{
name = "scroll",
function = L_Scroll,
},
new()
{
name = "getBlink",
function = L_GetBlink,
},
new()
{
name = "setBlink",
function = L_SetBlink,
},
new(),
};
public void LuaInit(Lua state)
{
state.RequireF("term", Open, false);
}
public int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(TermLib);
return 1;
}
public static void UpdateSize()
{
_game.Width = RealWidth;
_game.Height = RealHeight;
_game.UpdateSize();
CharGrid = new Char?[Width * Height];
}
public static Vector2 ToRealPos(Vector2 termPos)
{
return new(termPos.X * CharWidth, termPos.Y * CharHeight);
}
public static void PlotChar(Vector2 pos, char ch, Color? fgc = null, Color? bgc = null, bool save = true)
{
if (pos.X < 0 || pos.Y < 0 || pos.X >= Width || pos.Y >= Height)
return;
var fg = fgc ?? ForegroundColor;
var bg = bgc ?? BackgroundColor;
var realpos = ToRealPos(pos);
var charpos = realpos + CharOffset;
_game.Drawing.DrawRectangle(realpos, new(CharWidth, CharHeight), bg, Math.Min(CharWidth, CharHeight));
_game.Drawing.DrawString(charpos, ch.ToString(), fg);
if (!save)
return;
CharGrid[(int)pos.X + (int)pos.Y * Width] = new Char
{
Character = ch,
Foreground = ForegroundColor,
Background = BackgroundColor,
};
}
public static void RedrawPos(Vector2 pos)
{
if (pos.X < 0 || pos.Y < 0 || pos.X >= Width || pos.Y >= Height)
return;
var ch = CharGrid[(int)pos.X + (int)pos.Y * Width] ??
new Char
{
Character = ' ',
Foreground = ForegroundColor,
Background = BackgroundColor,
};
PlotChar(pos, ch.Character, ch.Foreground, ch.Background, false);
}
public static void RedrawAll()
{
for(int y = 0; y < Height; y++)
{
for(int x = 0; x < Width; x++)
{
RedrawPos(new(x, y));
}
}
}
public static void DumpScreen(bool clear = false)
{
if(clear)
Console.Clear();
Console.WriteLine("\n /{0}", new string('-', Width));
for (int i = 0; i < CharGrid.Length; i++)
{
if (i % Width == 0)
{
if (i > 0)
Console.Write("|\n");
Console.Write("{0,3}:", i / Width);
}
Console.Write(CharGrid[i]?.Character ?? ' ');
}
Console.WriteLine("|\n \\{0}", new string('-', Width));
}
private void OnTick(object sender, TickEvent e)
{
if (((int)e.TotalTicks % CursorDelay) == 0)
{
cursorState = !cursorState;
UpdateCursor();
}
}
private static void UpdateCursor()
{
if (!enableCursor)
return;
if (_cursorPosition.X < 0 || _cursorPosition.Y < 0 || _cursorPosition.X >= Width || _cursorPosition.Y >= Height)
return;
var ch = CharGrid[(int)_cursorPosition.X + (int)_cursorPosition.Y * Width] ??
new Char
{
Character = ' ',
Foreground = ForegroundColor,
Background = BackgroundColor,
};
Color fg, bg;
if (cursorState)
{
fg = ch.Background;
bg = ch.Foreground;
}
else
{
fg = ch.Foreground;
bg = ch.Background;
}
PlotChar(_cursorPosition, ch.Character, fg, bg, false);
}
private static void ClearGrid()
{
CharGrid = new Char?[CharGrid.Length];
_game.Drawing.Clear(BackgroundColor);
}
public static void Write(string text)
{
foreach (var ch in text)
{
PlotChar(_cursorPosition, ch);
_cursorPosition = new(_cursorPosition.X + 1, _cursorPosition.Y);
}
}
private static int L_Write(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var str = L.ToString(1);
Write(str);
return 0;
}
public static void SetCursorPosition(int x, int y)
{
RedrawPos(_cursorPosition);
cursorState = true;
_cursorPosition = new(x - 1, y - 1);
UpdateCursor();
}
private static int L_GetPos(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushInteger((int)_cursorPosition.X + 1);
L.PushInteger((int)_cursorPosition.Y + 1);
return 2;
}
private static int L_SetPos(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var x = L.CheckNumber(1);
var y = L.CheckNumber(2);
SetCursorPosition((int)x, (int)y);
return 0;
}
public static void SetSize(int width, int height)
{
Width = width;
Height = height;
UpdateSize();
}
private static int L_GetSize(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushInteger(Width);
L.PushInteger(Height);
return 2;
}
private static int L_SetSize(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var w = (int)L.CheckNumber(1);
var h = (int)L.CheckNumber(2);
if (w <= 0)
{
L.ArgumentError(1, "number must be greater than 0");
}
if (h <= 0)
{
L.ArgumentError(2, "number must be greater than 0");
}
SetSize(w, h);
return 0;
}
private static int L_GetForegroundColor(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushInteger(ForegroundColor.R);
L.PushInteger(ForegroundColor.G);
L.PushInteger(ForegroundColor.B);
return 3;
}
private static int L_SetForegroundColor(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var argsn = L.GetTop();
byte r, g, b;
// R, G, and B values
if (argsn == 3)
{
r = (byte)L.CheckNumber(1);
g = (byte)L.CheckNumber(2);
b = (byte)L.CheckNumber(3);
}
// packed RGB value
else if (argsn == 1)
{
var c = (uint)L.CheckInteger(1);
Utils.UnpackRGB(c, out r, out g, out b);
}
else
{
L.ArgumentError(argsn, "expected 1 or 3 number values");
return 0;
}
ForegroundColor = new(r, g, b);
return 0;
}
private static int L_GetBackgroundColor(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushInteger(BackgroundColor.R);
L.PushInteger(BackgroundColor.G);
L.PushInteger(BackgroundColor.B);
return 3;
}
private static int L_SetBackgroundColor(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var argsn = L.GetTop();
byte r, g, b;
// R, G, and B values
if (argsn == 3)
{
r = (byte)L.CheckNumber(1);
g = (byte)L.CheckNumber(2);
b = (byte)L.CheckNumber(3);
}
// packed RGB value
else if (argsn == 1)
{
var c = (uint)L.CheckInteger(1);
Utils.UnpackRGB(c, out r, out g, out b);
}
else
{
L.ArgumentError(argsn, "expected 1 or 3 number values");
return 0;
}
BackgroundColor = new(r, g, b);
return 0;
}
private static int L_ToReal(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var x = (int)L.CheckNumber(1);
var y = (int)L.CheckNumber(2);
L.PushInteger(x * CharWidth);
L.PushInteger(y * CharHeight);
return 2;
}
private static int L_FromReal(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var x = (int)L.CheckNumber(1);
var y = (int)L.CheckNumber(2);
L.PushInteger(x / CharWidth);
L.PushInteger(y / CharHeight);
return 2;
}
public static void Clear()
{
ClearGrid();
RedrawAll();
}
private static int L_Clear(IntPtr state)
{
var L = Lua.FromIntPtr(state);
Clear();
return 0;
}
public static void ClearLine()
{
for (int x = 0; x < Width; x++)
PlotChar(new(x, _cursorPosition.Y), ' ');
}
private static int L_ClearLine(IntPtr state)
{
var L = Lua.FromIntPtr(state);
ClearLine();
return 0;
}
public static void Scroll(int lines)
{
if (lines == 0)
return;
if (lines <= -Height || lines >= Height)
{
ClearGrid();
return;
}
var lineLength = Math.Abs(lines) * Width;
var newGrid = new Char?[CharGrid.Length];
if (lines < 0)
{
Array.Copy(CharGrid, lineLength, newGrid, 0, CharGrid.Length - lineLength);
}
else
{
Array.Copy(CharGrid, 0, newGrid, lineLength, CharGrid.Length - lineLength);
}
CharGrid = newGrid;
RedrawAll();
}
private static int L_Scroll(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var lines = L.CheckInteger(1) * -1;
Scroll((int)lines);
return 0;
}
public static void SetCursorBlink(bool enable)
{
enableCursor = enable;
if (!enableCursor)
{
RedrawPos(_cursorPosition);
}
}
private static int L_GetBlink(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.PushBoolean(enableCursor);
return 1;
}
private static int L_SetBlink(IntPtr state)
{
var L = Lua.FromIntPtr(state);
L.CheckType(1, LuaType.Boolean);
var enableCursor = L.ToBoolean(1);
SetCursorBlink(enableCursor);
return 0;
}
}

View file

@ -0,0 +1,77 @@
using Capy64.API;
using KeraLua;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime.Libraries;
class Timer : IPlugin
{
private LuaRegister[] TimerLib = new LuaRegister[]
{
new()
{
name = "start",
function = L_StartTimer,
},
new(),
};
private static IGame _game;
private static uint _timerId;
public Timer(IGame game)
{
_game = game;
_timerId = 0;
}
public void LuaInit(Lua state)
{
state.RequireF("timer", Open, false);
}
private int Open(IntPtr state)
{
var l = Lua.FromIntPtr(state);
l.NewLib(TimerLib);
return 1;
}
private static int L_StartTimer(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var delay = L.CheckInteger(1);
L.ArgumentCheck(delay > 0, 1, "delay must be greater than 0");
var timerId = _timerId++;
var timer = new System.Timers.Timer
{
AutoReset = false,
Enabled = true,
Interval = delay,
};
timer.Elapsed += (o, e) =>
{
_game.LuaRuntime.PushEvent("timer", timerId);
};
L.PushInteger(timerId);
return 1;
}
private static int L_Sleep(IntPtr state)
{
var L = Lua.FromIntPtr(state);
return 0;
}
}

View file

@ -0,0 +1,8 @@
namespace Capy64.LuaRuntime;
public struct LuaEvent
{
public string Name { get; set; }
public object[] Parameters { get; set; }
public bool BypassFilter { get; set; }
}

View file

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime;
public class LuaException : Exception
{
public LuaException()
{
}
public LuaException(string message) : base(message)
{
}
public LuaException(string message, Exception innerException) : base(message, innerException)
{
}
protected LuaException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}

View file

@ -0,0 +1,137 @@
using Capy64.LuaRuntime.Extensions;
using Capy64.LuaRuntime.Libraries;
using KeraLua;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
namespace Capy64.LuaRuntime;
public class Runtime
{
private readonly ConcurrentQueue<LuaEvent> eventQueue = new(new LuaEvent[]
{
new()
{
Name = "init",
Parameters = Array.Empty<object>()
}
});
private readonly Lua Parent;
public Lua Thread { get; private set; }
private string[] filters = Array.Empty<string>();
private bool Disposing = false;
public Runtime()
{
Parent = new(false)
{
Encoding = Encoding.UTF8,
};
Sandbox.OpenLibraries(Parent);
Thread = Parent.NewThread();
}
public void Patch()
{
Sandbox.Patch(Parent);
}
public void Init()
{
var initContent = File.ReadAllText(Path.Combine(FileSystem.DataPath, "init.lua"));
var status = Thread.LoadString(initContent, "=init.lua");
if (status != LuaStatus.OK)
{
throw new LuaException(Thread.ToString(-1));
}
}
public void PushEvent(LuaEvent ev)
{
eventQueue.Enqueue(ev);
}
public void PushEvent(string name, params object[] pars)
{
eventQueue.Enqueue(new() { Name = name, Parameters = pars });
}
/// <summary>
/// Resume the Lua thread
/// </summary>
/// <returns>Whether it yielded</returns>
public bool Resume()
{
while (eventQueue.TryDequeue(out LuaEvent ev))
{
if (!ResumeThread(ev))
return false;
}
return true;
}
private bool ResumeThread(LuaEvent ev)
{
if (filters.Length > 0 && !filters.Contains(ev.Name))
{
if (!ev.BypassFilter)
{
return true;
}
}
filters = Array.Empty<string>();
var evpars = PushEventToStack(ev);
if (Disposing)
return false;
var status = Thread.Resume(null, evpars, out int pars);
if (status is LuaStatus.OK or LuaStatus.Yield)
{
if(Disposing)
return false;
filters = new string[pars];
for (int i = 0; i < pars; i++)
{
filters[i] = Thread.OptString(i + 1, null);
}
Thread.Pop(pars);
return status == LuaStatus.Yield;
}
var error = Thread.OptString(-1, "Unknown exception");
Thread.Traceback(Thread);
var stacktrace = Thread.OptString(-1, "");
throw new LuaException($"Top thread exception:\n{error}\n{stacktrace}");
}
private int PushEventToStack(LuaEvent ev)
{
Thread.PushString(ev.Name);
if (ev.Parameters != null)
{
foreach (var par in ev.Parameters)
{
Thread.PushValue(par);
}
}
return (ev.Parameters?.Length ?? 0) + 1;
}
public void Close()
{
Disposing = true;
Parent.Close();
}
}

View file

@ -0,0 +1,304 @@
using Capy64.LuaRuntime.Extensions;
using Capy64.LuaRuntime.Libraries;
using KeraLua;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.LuaRuntime;
internal class Sandbox
{
private struct CallData
{
public int Delta;
}
internal static void OpenLibraries(Lua L)
{
L.OpenBase();
L.OpenCoroutine();
L.OpenDebug();
L.OpenMath();
L.OpenString();
L.OpenTable();
L.OpenUTF8();
L.OpenOS();
L.OpenPackage();
}
internal static void Patch(Lua L)
{
// patch package
L.GetGlobal("package");
// package.cpatch: empty paths
L.PushString("cpath");
L.PushString("");
L.SetTable(-3);
// package.path: local paths only
L.PushString("path");
L.PushString("./?.lua;./?/init.lua");
L.SetTable(-3);
// package.loadlib: remove func to load C libs
L.PushString("loadlib");
L.PushNil();
L.SetTable(-3);
L.PushString("searchpath");
L.PushCFunction(L_PatchedSearchpath);
L.SetTable(-3);
// package.config: apply unix directory separator
L.PushString("config");
L.GetTable(-2);
var packageConfig = L.ToString(-1);
packageConfig = string.Concat("/", packageConfig.AsSpan(1));
L.PushString("config");
L.PushString(packageConfig);
L.SetTable(-4);
// delete 3 and 4 searchers
L.PushString("searchers");
L.GetTable(-3);
L.PushNil();
L.SetInteger(-2, 3);
L.PushNil();
L.SetInteger(-2, 4);
// replace searcher 2 with a custom sandboxed one
L.PushCFunction(L_Searcher);
L.SetInteger(-2, 2);
L.Pop(L.GetTop());
// Replace loadfile with sandboxed one
L.PushCFunction(L_Loadfile);
L.SetGlobal("loadfile");
// Replace dofile with sandboxed one
L.PushCFunction(L_Dofile);
L.SetGlobal("dofile");
L.Pop(L.GetTop());
// yeet dangerous os functions
L.GetGlobal("os");
L.PushString("execute");
L.PushNil();
L.SetTable(-3);
L.PushString("tmpname");
L.PushNil();
L.SetTable(-3);
L.PushString("remove");
L.PushNil();
L.SetTable(-3);
L.PushString("rename");
L.PushNil();
L.SetTable(-3);
L.PushString("getenv");
L.PushNil();
L.SetTable(-3);
}
internal static int L_Searcher(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var libname = L.CheckString(1);
L.GetGlobal("package");
// get searcher path as arg for package.searchpath
L.PushString("path");
L.GetTable(-2);
var searchpath = L.ToString(-1);
// get package.searchpath
L.PushString("searchpath");
L.GetTable(-3);
L.PushString(libname);
L.PushString(searchpath);
L.Call(2, 2);
if (L.IsNil(-2))
{
return 1;
}
// path resolved by package.searchpath
var foundPath = L.ToString(-2);
L.GetGlobal("loadfile");
L.PushString(foundPath);
L.Call(1, 2);
if (L.IsNil(-2))
{
var errorMessage = new StringBuilder();
errorMessage.AppendFormat("error loading module '{0}' from file '{1}':", libname, foundPath);
errorMessage.Append('\t');
errorMessage.AppendLine(L.ToString(-1));
L.PushString(errorMessage.ToString());
return 1;
}
L.Remove(-1);
L.PushString(foundPath);
return 2;
}
internal static int L_PatchedSearchpath(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var libname = L.CheckString(1);
var searchpath = L.CheckString(2);
var possiblePaths = searchpath
.Split(';')
.Select(p => p.Replace("?", libname));
var errorMessage = new StringBuilder();
foreach (var possiblePath in possiblePaths)
{
var path = FileSystem.Resolve(possiblePath);
var info = new FileInfo(path);
if (!info.Exists)
{
errorMessage.AppendLine(string.Format("no file '{0}'", possiblePath));
continue;
}
try
{
File.Open(path, FileMode.Open, FileAccess.Read).Dispose();
}
catch
{
errorMessage.AppendLine(string.Format("error opening file '{0}'", possiblePath));
continue;
}
L.PushString(possiblePath);
return 1;
}
L.PushNil();
L.PushString(errorMessage.ToString());
return 2;
}
internal static int L_Loadfile(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var filename = L.CheckString(1);
bool hasMode = !L.IsNone(2);
bool hasEnv = !L.IsNone(3);
var path = FileSystem.Resolve(filename);
var fileInfo = new FileInfo(path);
if (!fileInfo.Exists)
{
L.PushNil();
L.PushString($"cannot open {filename}: No such file or directory");
return 2;
}
else if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
{
L.PushNil();
L.PushString($"cannot read {filename}: Is a directory");
return 2;
}
var chunk = File.ReadAllBytes(path);
L.GetGlobal("load");
L.PushBuffer(chunk);
L.PushString(filename);
var values = 2;
if (hasMode)
{
L.PushCopy(2);
values++;
}
if (hasEnv)
{
L.PushCopy(3);
values++;
}
L.Call(values, 2);
return 2;
}
internal static int LK_Dofile(IntPtr state, int status, IntPtr ctx)
{
var L = Lua.FromIntPtr(state);
var nargs = L.GetTop();
return nargs;
}
internal static int L_Dofile(IntPtr state)
{
var L = Lua.FromIntPtr(state);
var filename = L.CheckString(1);
L.GetGlobal("loadfile");
L.PushString(filename);
L.Call(1, 2);
if (L.IsNil(-2))
{
L.Error(L.ToString(-1));
return 0;
}
L.PushCopy(-2);
L.Insert(1);
L.SetTop(1);
L.CallK(0, Constants.MULTRET, 0, LK_Dofile);
return 0;
}
internal static void DumpStack(Lua state)
{
var n = state.GetTop();
for (int i = n; i >= 1; i--)
{
Console.WriteLine("{0,4}: {1}", -i, state.ToString(-i));
}
Console.WriteLine("--------------");
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
namespace Capy64.PluginManager
{
class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}

View file

@ -0,0 +1,44 @@
using Capy64.API;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Capy64.PluginManager;
internal class PluginLoader
{
private static Assembly LoadPlugin(string fileName)
{
var path = Path.Combine(Environment.CurrentDirectory, fileName);
var loadContext = new PluginLoadContext(path);
return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(path)));
}
public static List<IPlugin> LoadAllPlugins(string pluginsPath, IServiceProvider provider)
{
if (!Directory.Exists(pluginsPath))
Directory.CreateDirectory(pluginsPath);
var plugins = new List<IPlugin>();
foreach (var fileName in Directory.GetFiles(pluginsPath).Where(q => q.EndsWith(".dll")))
{
var assembly = LoadPlugin(fileName);
foreach (Type type in assembly.GetTypes())
{
if (typeof(IPlugin).IsAssignableFrom(type))
{
IPlugin result = ActivatorUtilities.CreateInstance(provider, type) as IPlugin;
plugins.Add(result);
}
}
}
return plugins;
}
}

15
Capy64/Program.cs Normal file
View file

@ -0,0 +1,15 @@
using Capy64;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using var game = new Capy64.Capy64();
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IGame>(game);
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();

View file

@ -0,0 +1,11 @@
{
"profiles": {
"WSL": {
"commandName": "WSL2",
"distributionName": ""
},
"Capy64": {
"commandName": "Project"
}
}
}

21
Capy64/Utils.cs Normal file
View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Capy64;
public static class Utils
{
public struct Borders
{
public int Top, Bottom, Left, Right;
}
public static void UnpackRGB(uint packed, out byte r, out byte g, out byte b)
{
b = (byte)(packed & 0xff);
g = (byte)(packed >> 8 & 0xff);
r = (byte)(packed >> 16 & 0xff);
}
}

62
Capy64/Worker.cs Normal file
View file

@ -0,0 +1,62 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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()
{
}
}

43
Capy64/app.manifest Normal file
View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Capy64"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on and is
is designed to work with. Uncomment the appropriate elements and Windows will
automatically selected the most compatible environment. -->
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>