commit 84cba789a36340e9843873abf6d356aa2329e186 Author: Alessandro Proto Date: Sat Jan 7 16:59:47 2023 +0100 Add project files. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Capy64.sln b/Capy64.sln new file mode 100644 index 0000000..5b1d463 --- /dev/null +++ b/Capy64.sln @@ -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 diff --git a/Capy64/.config/dotnet-tools.json b/Capy64/.config/dotnet-tools.json new file mode 100644 index 0000000..efabe22 --- /dev/null +++ b/Capy64/.config/dotnet-tools.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/Capy64/API/IPlugin.cs b/Capy64/API/IPlugin.cs new file mode 100644 index 0000000..941b54c --- /dev/null +++ b/Capy64/API/IPlugin.cs @@ -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) { } + +} diff --git a/Capy64/Assets/Lua/init.lua b/Capy64/Assets/Lua/init.lua new file mode 100644 index 0000000..d0a6f84 --- /dev/null +++ b/Capy64/Assets/Lua/init.lua @@ -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 \ No newline at end of file diff --git a/Capy64/Assets/Lua/lib/colors.lua b/Capy64/Assets/Lua/lib/colors.lua new file mode 100644 index 0000000..5a2aca7 --- /dev/null +++ b/Capy64/Assets/Lua/lib/colors.lua @@ -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; \ No newline at end of file diff --git a/Capy64/Assets/Lua/lib/event.lua b/Capy64/Assets/Lua/lib/event.lua new file mode 100644 index 0000000..b15af13 --- /dev/null +++ b/Capy64/Assets/Lua/lib/event.lua @@ -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 \ No newline at end of file diff --git a/Capy64/Assets/Lua/lib/expect.lua b/Capy64/Assets/Lua/lib/expect.lua new file mode 100644 index 0000000..483f757 --- /dev/null +++ b/Capy64/Assets/Lua/lib/expect.lua @@ -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 }) diff --git a/Capy64/Assets/Lua/lib/utfstring.lua b/Capy64/Assets/Lua/lib/utfstring.lua new file mode 100644 index 0000000..7119373 --- /dev/null +++ b/Capy64/Assets/Lua/lib/utfstring.lua @@ -0,0 +1,1762 @@ +local expect = require "expect" + +local UTFString = {} +local UTFString_mt = {__index = UTFString, __name = "UTFString"} + +local function toUTF8(s) + -- convert from ANSI if the string is invalid in UTF-8 + local ss = "" + local iter, invar, i = utf8.codes(s) + local ok, c + while i do + local oi = i + ok, i, c = pcall(iter, invar, i) + if not ok then i, ss = oi + 1, ss .. utf8.char(string.byte(s, utf8.offset(s, oi) + 1)) + elseif c then ss = ss .. utf8.char(c) end + end + return ss +end + +function UTFString:new(s) + return setmetatable({str = toUTF8(s)}, UTFString_mt) +end + +setmetatable(UTFString, {__call = UTFString.new}) + +local utf8_case_conv_utl, utf8_case_conv_ltu = nil, {} -- defined at bottom of file + +-- string library implementation + +function UTFString:byte(i, j) + expect(1, i, "number", "nil") + expect(2, j, "number", "nil") + return utf8.codepoint(self.str, i, j) +end + +UTFString.char = utf8.char +UTFString.dump = string.dump + +function UTFString:find(pattern, init, plain) + expect(1, pattern, "string", "table") + expect(2, init, "number", "nil") + if type(pattern) == "table" then + if getmetatable(pattern) ~= UTFString_mt then error("bad argument #1 (expected string or UTFString, got table)", 2) end + pattern = pattern.str + else pattern = toUTF8(pattern) end + pattern = string.gsub(string.gsub(pattern, "(.)%.", function(s) if s == "%" then return "%." else return s .. utf8.charpattern end end), "^%.", utf8.charpattern) + local s, e = string.find(self.str, pattern, init and utf8.offset(self.str, init), plain) + if not s then return nil end + return utf8.len(string.sub(self.str, 1, s - 1)) + 1, utf8.len(string.sub(self.str, 1, e - 1)) + 1 +end + +function UTFString:format(...) + local args = table.pack(...) + for i = 1, args.n do + if type(args[i]) == "table" and getmetatable(args[i]) == UTFString_mt then args[i] = args[i].str + elseif type(args[i]) == "string" then args[i] = toUTF8(args[i]) end + end + return UTFString(string.format(self.str, table.unpack(args, 1, args.n))) +end + +function UTFString:gmatch(pattern) + expect(1, pattern, "string", "table") + if type(pattern) == "table" then + if getmetatable(pattern) ~= UTFString_mt then error("bad argument #1 (expected string or UTFString, got table)", 2) end + pattern = pattern.str + else pattern = toUTF8(pattern) end + pattern = string.gsub(string.gsub(pattern, "(.)%.", function(s) if s == "%" then return "%." else return s .. utf8.charpattern end end), "^%.", utf8.charpattern) + local iter = string.gmatch(self.str, pattern) + return function() + local matches = table.pack(iter()) + for i = 1, matches.n do + local tt = type(matches[i]) + if tt == "string" then matches[i] = UTFString(matches[i]) + elseif tt == "number" then matches[i] = utf8.len(string.sub(self.str, 1, matches[i])) + 1 end + end + return table.unpack(matches, 1, matches.n) + end +end + +function UTFString:gsub(pattern, repl, n) + expect(1, pattern, "string", "table") + expect(2, repl, "string", "table", "function") + expect(3, n, "number", "nil") + if type(pattern) == "table" then + if getmetatable(pattern) ~= UTFString_mt then error("bad argument #1 (expected string or UTFString, got table)", 2) end + pattern = pattern.str + else pattern = toUTF8(pattern)end + if type(repl) == "table" and getmetatable(repl) == UTFString_mt then repl = repl.str + elseif type(repl) == "string" then repl = toUTF8(repl) end + pattern = string.gsub(string.gsub(pattern, "(.)%.", function(s) if s == "%" then return "%." else return s .. utf8.charpattern end end), "^%.", utf8.charpattern) + local s, total = string.gsub(self.str, pattern, repl, n) + return UTFString(s), total +end + +function UTFString:len() + return utf8.len(self.str) +end + +function UTFString:lower() + local s = "" + for _, c in utf8.codes(self.str) do + if utf8_case_conv_utl[c] then + if type(utf8_case_conv_utl[c]) == "table" then s = s .. utf8.char(table.unpack(utf8_case_conv_utl[c])) + else s = s .. utf8.char(utf8_case_conv_utl[c]) end + else s = s .. utf8.char(c) end + end + return UTFString(s) +end + +function UTFString:match(pattern, init) + expect(1, pattern, "string", "table") + expect(2, init, "number", "nil") + if type(pattern) == "table" then + if getmetatable(pattern) ~= UTFString_mt then error("bad argument #1 (expected string or UTFString, got table)", 2) end + pattern = pattern.str + else pattern = toUTF8(pattern) end + pattern = string.gsub(string.gsub(pattern, "(.)%.", function(s) if s == "%" then return "%." else return s .. utf8.charpattern end end), "^%.", utf8.charpattern) + local matches = table.pack(string.match(self.str, pattern, init and utf8.offset(self.str, init))) + for i = 1, matches.n do + local tt = type(matches[i]) + if tt == "string" then matches[i] = UTFString(matches[i]) + elseif tt == "number" then matches[i] = utf8.len(string.sub(self.str, 1, matches[i])) + 1 end + end + return table.unpack(matches, 1, matches.n) +end + +function UTFString:pack(...) + return UTFString(string.pack(self.str, ...)) +end + +function UTFString:packsize() + return string.packsize(self.str) +end + +function UTFString:rep(n, sep) + expect(1, n, "number") + expect(2, sep, "string", "table", "nil") + if type(sep) == "table" then + if getmetatable(sep) ~= UTFString_mt then error("bad argument #2 (expected string or UTFString, got table)", 2) end + sep = sep.str + elseif sep then sep = toUTF8(sep) end + return UTFString(string.rep(self.str, n, sep)) +end + +function UTFString:reverse() + local codes = {n = 0} + for _, c in utf8.codes(self.str) do + codes.n = codes.n + 1 + codes[codes.n] = c + end + local s = "" + for i = codes.n, 1, -1 do s = s .. utf8.char(codes[i]) end + return UTFString(s) +end + +function UTFString:sub(i, j) + expect(1, i, "number") + expect(2, j, "number", "nil") + return UTFString(string.sub(self.str, utf8.offset(self.str, i), j and utf8.offset(self.str, j))) +end + +function UTFString:unpack(s, pos) + expect(1, s, "string", "table") + expect(2, pos, "number", "nil") + if type(s) == "table" then + if getmetatable(s) ~= UTFString_mt then error("bad argument #1 (expected string or UTFString, got table)", 2) end + s = s.str + if pos then pos = utf8.offset(s, pos) end + end + return string.unpack(self.str, s, pos) +end + +function UTFString:upper() + local s = "" + for _, c in utf8.codes(self.str) do + if utf8_case_conv_ltu[c] then + if type(utf8_case_conv_ltu[c]) == "table" then s = s .. utf8.char(table.unpack(utf8_case_conv_ltu[c])) + else s = s .. utf8.char(utf8_case_conv_ltu[c]) end + else s = s .. utf8.char(c) end + end + return UTFString(s) +end + +-- metamethods + +function UTFString_mt:__len() + return utf8.len(self.str) +end + +function UTFString_mt.__concat(a, b) + if type(a) == "string" then return UTFString(a .. b.str) + elseif type(b) == "string" then return UTFString(a.str .. b) + else + if type(a) ~= "table" or getmetatable(a) ~= UTFString_mt then error("attempt to concatenate " .. type(a) .. " and UTFString", 2) + elseif type(b) ~= "table" or getmetatable(b) ~= UTFString_mt then error("attempt to concatenate UTFString and " .. type(b), 2) end + return UTFString(a.str .. b.str) + end +end + +function UTFString_mt.__eq(a, b) + if type(a) == "string" then return a == b.str + elseif type(b) == "string" then return a.str == b + elseif type(a) ~= "table" or type(b) ~= "table" or getmetatable(a) ~= UTFString_mt or getmetatable(b) ~= UTFString_mt then return false + else return a.str == b.str end +end + +function UTFString_mt.__lt(a, b) + if type(a) == "string" then return a < b.str + elseif type(b) == "string" then return a.str < b + else + if type(a) ~= "table" or getmetatable(a) ~= UTFString_mt then error("attempt to compare " .. type(a) .. " and UTFString", 2) + elseif type(b) ~= "table" or getmetatable(b) ~= UTFString_mt then error("attempt to compare UTFString and " .. type(b), 2) end + return a.str < b.str + end +end + +function UTFString_mt.__le(a, b) + if type(a) == "string" then return a <= b.str + elseif type(b) == "string" then return a.str <= b + else + if type(a) ~= "table" or getmetatable(a) ~= UTFString_mt then error("attempt to compare " .. type(a) .. " and UTFString", 2) + elseif type(b) ~= "table" or getmetatable(b) ~= UTFString_mt then error("attempt to compare UTFString and " .. type(b), 2) end + return a.str <= b.str + end +end + +function UTFString_mt:__tostring() + local str = "" + for _, c in utf8.codes(self.str) do + if c > 255 then str = str .. "?" + else str = str .. string.char(c) end + end + return str +end + +-- tables for UTF-8 case conversion + +utf8_case_conv_utl = { + [0x0041] = 0x0061, + [0x0042] = 0x0062, + [0x0043] = 0x0063, + [0x0044] = 0x0064, + [0x0045] = 0x0065, + [0x0046] = 0x0066, + [0x0047] = 0x0067, + [0x0048] = 0x0068, + [0x0049] = 0x0069, + + [0x004A] = 0x006A, + [0x004B] = 0x006B, + [0x004C] = 0x006C, + [0x004D] = 0x006D, + [0x004E] = 0x006E, + [0x004F] = 0x006F, + [0x0050] = 0x0070, + [0x0051] = 0x0071, + [0x0052] = 0x0072, + [0x0053] = 0x0073, + [0x0054] = 0x0074, + [0x0055] = 0x0075, + [0x0056] = 0x0076, + [0x0057] = 0x0077, + [0x0058] = 0x0078, + [0x0059] = 0x0079, + [0x005A] = 0x007A, + [0x00B5] = 0x03BC, + [0x00C0] = 0x00E0, + [0x00C1] = 0x00E1, + [0x00C2] = 0x00E2, + [0x00C3] = 0x00E3, + [0x00C4] = 0x00E4, + [0x00C5] = 0x00E5, + [0x00C6] = 0x00E6, + [0x00C7] = 0x00E7, + [0x00C8] = 0x00E8, + [0x00C9] = 0x00E9, + [0x00CA] = 0x00EA, + [0x00CB] = 0x00EB, + [0x00CC] = 0x00EC, + [0x00CD] = 0x00ED, + [0x00CE] = 0x00EE, + [0x00CF] = 0x00EF, + [0x00D0] = 0x00F0, + [0x00D1] = 0x00F1, + [0x00D2] = 0x00F2, + [0x00D3] = 0x00F3, + [0x00D4] = 0x00F4, + [0x00D5] = 0x00F5, + [0x00D6] = 0x00F6, + [0x00D8] = 0x00F8, + [0x00D9] = 0x00F9, + [0x00DA] = 0x00FA, + [0x00DB] = 0x00FB, + [0x00DC] = 0x00FC, + [0x00DD] = 0x00FD, + [0x00DE] = 0x00FE, + [0x00DF] = {0x0073, 0x0073}, + [0x0100] = 0x0101, + [0x0102] = 0x0103, + [0x0104] = 0x0105, + [0x0106] = 0x0107, + [0x0108] = 0x0109, + [0x010A] = 0x010B, + [0x010C] = 0x010D, + [0x010E] = 0x010F, + [0x0110] = 0x0111, + [0x0112] = 0x0113, + [0x0114] = 0x0115, + [0x0116] = 0x0117, + [0x0118] = 0x0119, + [0x011A] = 0x011B, + [0x011C] = 0x011D, + [0x011E] = 0x011F, + [0x0120] = 0x0121, + [0x0122] = 0x0123, + [0x0124] = 0x0125, + [0x0126] = 0x0127, + [0x0128] = 0x0129, + [0x012A] = 0x012B, + [0x012C] = 0x012D, + [0x012E] = 0x012F, + [0x0130] = {0x0069, 0x0307}, + + [0x0132] = 0x0133, + [0x0134] = 0x0135, + [0x0136] = 0x0137, + [0x0139] = 0x013A, + [0x013B] = 0x013C, + [0x013D] = 0x013E, + [0x013F] = 0x0140, + [0x0141] = 0x0142, + [0x0143] = 0x0144, + [0x0145] = 0x0146, + [0x0147] = 0x0148, + [0x0149] = {0x02BC, 0x006E}, + [0x014A] = 0x014B, + [0x014C] = 0x014D, + [0x014E] = 0x014F, + [0x0150] = 0x0151, + [0x0152] = 0x0153, + [0x0154] = 0x0155, + [0x0156] = 0x0157, + [0x0158] = 0x0159, + [0x015A] = 0x015B, + [0x015C] = 0x015D, + [0x015E] = 0x015F, + [0x0160] = 0x0161, + [0x0162] = 0x0163, + [0x0164] = 0x0165, + [0x0166] = 0x0167, + [0x0168] = 0x0169, + [0x016A] = 0x016B, + [0x016C] = 0x016D, + [0x016E] = 0x016F, + [0x0170] = 0x0171, + [0x0172] = 0x0173, + [0x0174] = 0x0175, + [0x0176] = 0x0177, + [0x0178] = 0x00FF, + [0x0179] = 0x017A, + [0x017B] = 0x017C, + [0x017D] = 0x017E, + [0x017F] = 0x0073, + [0x0181] = 0x0253, + [0x0182] = 0x0183, + [0x0184] = 0x0185, + [0x0186] = 0x0254, + [0x0187] = 0x0188, + [0x0189] = 0x0256, + [0x018A] = 0x0257, + [0x018B] = 0x018C, + [0x018E] = 0x01DD, + [0x018F] = 0x0259, + [0x0190] = 0x025B, + [0x0191] = 0x0192, + [0x0193] = 0x0260, + [0x0194] = 0x0263, + [0x0196] = 0x0269, + [0x0197] = 0x0268, + [0x0198] = 0x0199, + [0x019C] = 0x026F, + [0x019D] = 0x0272, + [0x019F] = 0x0275, + [0x01A0] = 0x01A1, + [0x01A2] = 0x01A3, + [0x01A4] = 0x01A5, + [0x01A6] = 0x0280, + [0x01A7] = 0x01A8, + [0x01A9] = 0x0283, + [0x01AC] = 0x01AD, + [0x01AE] = 0x0288, + [0x01AF] = 0x01B0, + [0x01B1] = 0x028A, + [0x01B2] = 0x028B, + [0x01B3] = 0x01B4, + [0x01B5] = 0x01B6, + [0x01B7] = 0x0292, + [0x01B8] = 0x01B9, + [0x01BC] = 0x01BD, + [0x01C4] = 0x01C6, + [0x01C5] = 0x01C6, + [0x01C7] = 0x01C9, + [0x01C8] = 0x01C9, + [0x01CA] = 0x01CC, + [0x01CB] = 0x01CC, + [0x01CD] = 0x01CE, + [0x01CF] = 0x01D0, + [0x01D1] = 0x01D2, + [0x01D3] = 0x01D4, + [0x01D5] = 0x01D6, + [0x01D7] = 0x01D8, + [0x01D9] = 0x01DA, + [0x01DB] = 0x01DC, + [0x01DE] = 0x01DF, + [0x01E0] = 0x01E1, + [0x01E2] = 0x01E3, + [0x01E4] = 0x01E5, + [0x01E6] = 0x01E7, + [0x01E8] = 0x01E9, + [0x01EA] = 0x01EB, + [0x01EC] = 0x01ED, + [0x01EE] = 0x01EF, + [0x01F0] = {0x006A, 0x030C}, + [0x01F1] = 0x01F3, + [0x01F2] = 0x01F3, + [0x01F4] = 0x01F5, + [0x01F6] = 0x0195, + [0x01F7] = 0x01BF, + [0x01F8] = 0x01F9, + [0x01FA] = 0x01FB, + [0x01FC] = 0x01FD, + [0x01FE] = 0x01FF, + [0x0200] = 0x0201, + [0x0202] = 0x0203, + [0x0204] = 0x0205, + [0x0206] = 0x0207, + [0x0208] = 0x0209, + [0x020A] = 0x020B, + [0x020C] = 0x020D, + [0x020E] = 0x020F, + [0x0210] = 0x0211, + [0x0212] = 0x0213, + [0x0214] = 0x0215, + [0x0216] = 0x0217, + [0x0218] = 0x0219, + [0x021A] = 0x021B, + [0x021C] = 0x021D, + [0x021E] = 0x021F, + [0x0220] = 0x019E, + [0x0222] = 0x0223, + [0x0224] = 0x0225, + [0x0226] = 0x0227, + [0x0228] = 0x0229, + [0x022A] = 0x022B, + [0x022C] = 0x022D, + [0x022E] = 0x022F, + [0x0230] = 0x0231, + [0x0232] = 0x0233, + [0x023A] = 0x2C65, + [0x023B] = 0x023C, + [0x023D] = 0x019A, + [0x023E] = 0x2C66, + [0x0241] = 0x0242, + [0x0243] = 0x0180, + [0x0244] = 0x0289, + [0x0245] = 0x028C, + [0x0246] = 0x0247, + [0x0248] = 0x0249, + [0x024A] = 0x024B, + [0x024C] = 0x024D, + [0x024E] = 0x024F, + [0x0345] = 0x03B9, + [0x0370] = 0x0371, + [0x0372] = 0x0373, + [0x0376] = 0x0377, + [0x037F] = 0x03F3, + [0x0386] = 0x03AC, + [0x0388] = 0x03AD, + [0x0389] = 0x03AE, + [0x038A] = 0x03AF, + [0x038C] = 0x03CC, + [0x038E] = 0x03CD, + [0x038F] = 0x03CE, + [0x0390] = {0x03B9, 0x0308, 0x0301}, + [0x0391] = 0x03B1, + [0x0392] = 0x03B2, + [0x0393] = 0x03B3, + [0x0394] = 0x03B4, + [0x0395] = 0x03B5, + [0x0396] = 0x03B6, + [0x0397] = 0x03B7, + [0x0398] = 0x03B8, + [0x0399] = 0x03B9, + [0x039A] = 0x03BA, + [0x039B] = 0x03BB, + [0x039C] = 0x03BC, + [0x039D] = 0x03BD, + [0x039E] = 0x03BE, + [0x039F] = 0x03BF, + [0x03A0] = 0x03C0, + [0x03A1] = 0x03C1, + [0x03A3] = 0x03C3, + [0x03A4] = 0x03C4, + [0x03A5] = 0x03C5, + [0x03A6] = 0x03C6, + [0x03A7] = 0x03C7, + [0x03A8] = 0x03C8, + [0x03A9] = 0x03C9, + [0x03AA] = 0x03CA, + [0x03AB] = 0x03CB, + [0x03B0] = {0x03C5, 0x0308, 0x0301}, + [0x03C2] = 0x03C3, + [0x03CF] = 0x03D7, + [0x03D0] = 0x03B2, + [0x03D1] = 0x03B8, + [0x03D5] = 0x03C6, + [0x03D6] = 0x03C0, + [0x03D8] = 0x03D9, + [0x03DA] = 0x03DB, + [0x03DC] = 0x03DD, + [0x03DE] = 0x03DF, + [0x03E0] = 0x03E1, + [0x03E2] = 0x03E3, + [0x03E4] = 0x03E5, + [0x03E6] = 0x03E7, + [0x03E8] = 0x03E9, + [0x03EA] = 0x03EB, + [0x03EC] = 0x03ED, + [0x03EE] = 0x03EF, + [0x03F0] = 0x03BA, + [0x03F1] = 0x03C1, + [0x03F4] = 0x03B8, + [0x03F5] = 0x03B5, + [0x03F7] = 0x03F8, + [0x03F9] = 0x03F2, + [0x03FA] = 0x03FB, + [0x03FD] = 0x037B, + [0x03FE] = 0x037C, + [0x03FF] = 0x037D, + [0x0400] = 0x0450, + [0x0401] = 0x0451, + [0x0402] = 0x0452, + [0x0403] = 0x0453, + [0x0404] = 0x0454, + [0x0405] = 0x0455, + [0x0406] = 0x0456, + [0x0407] = 0x0457, + [0x0408] = 0x0458, + [0x0409] = 0x0459, + [0x040A] = 0x045A, + [0x040B] = 0x045B, + [0x040C] = 0x045C, + [0x040D] = 0x045D, + [0x040E] = 0x045E, + [0x040F] = 0x045F, + [0x0410] = 0x0430, + [0x0411] = 0x0431, + [0x0412] = 0x0432, + [0x0413] = 0x0433, + [0x0414] = 0x0434, + [0x0415] = 0x0435, + [0x0416] = 0x0436, + [0x0417] = 0x0437, + [0x0418] = 0x0438, + [0x0419] = 0x0439, + [0x041A] = 0x043A, + [0x041B] = 0x043B, + [0x041C] = 0x043C, + [0x041D] = 0x043D, + [0x041E] = 0x043E, + [0x041F] = 0x043F, + [0x0420] = 0x0440, + [0x0421] = 0x0441, + [0x0422] = 0x0442, + [0x0423] = 0x0443, + [0x0424] = 0x0444, + [0x0425] = 0x0445, + [0x0426] = 0x0446, + [0x0427] = 0x0447, + [0x0428] = 0x0448, + [0x0429] = 0x0449, + [0x042A] = 0x044A, + [0x042B] = 0x044B, + [0x042C] = 0x044C, + [0x042D] = 0x044D, + [0x042E] = 0x044E, + [0x042F] = 0x044F, + [0x0460] = 0x0461, + [0x0462] = 0x0463, + [0x0464] = 0x0465, + [0x0466] = 0x0467, + [0x0468] = 0x0469, + [0x046A] = 0x046B, + [0x046C] = 0x046D, + [0x046E] = 0x046F, + [0x0470] = 0x0471, + [0x0472] = 0x0473, + [0x0474] = 0x0475, + [0x0476] = 0x0477, + [0x0478] = 0x0479, + [0x047A] = 0x047B, + [0x047C] = 0x047D, + [0x047E] = 0x047F, + [0x0480] = 0x0481, + [0x048A] = 0x048B, + [0x048C] = 0x048D, + [0x048E] = 0x048F, + [0x0490] = 0x0491, + [0x0492] = 0x0493, + [0x0494] = 0x0495, + [0x0496] = 0x0497, + [0x0498] = 0x0499, + [0x049A] = 0x049B, + [0x049C] = 0x049D, + [0x049E] = 0x049F, + [0x04A0] = 0x04A1, + [0x04A2] = 0x04A3, + [0x04A4] = 0x04A5, + [0x04A6] = 0x04A7, + [0x04A8] = 0x04A9, + [0x04AA] = 0x04AB, + [0x04AC] = 0x04AD, + [0x04AE] = 0x04AF, + [0x04B0] = 0x04B1, + [0x04B2] = 0x04B3, + [0x04B4] = 0x04B5, + [0x04B6] = 0x04B7, + [0x04B8] = 0x04B9, + [0x04BA] = 0x04BB, + [0x04BC] = 0x04BD, + [0x04BE] = 0x04BF, + [0x04C0] = 0x04CF, + [0x04C1] = 0x04C2, + [0x04C3] = 0x04C4, + [0x04C5] = 0x04C6, + [0x04C7] = 0x04C8, + [0x04C9] = 0x04CA, + [0x04CB] = 0x04CC, + [0x04CD] = 0x04CE, + [0x04D0] = 0x04D1, + [0x04D2] = 0x04D3, + [0x04D4] = 0x04D5, + [0x04D6] = 0x04D7, + [0x04D8] = 0x04D9, + [0x04DA] = 0x04DB, + [0x04DC] = 0x04DD, + [0x04DE] = 0x04DF, + [0x04E0] = 0x04E1, + [0x04E2] = 0x04E3, + [0x04E4] = 0x04E5, + [0x04E6] = 0x04E7, + [0x04E8] = 0x04E9, + [0x04EA] = 0x04EB, + [0x04EC] = 0x04ED, + [0x04EE] = 0x04EF, + [0x04F0] = 0x04F1, + [0x04F2] = 0x04F3, + [0x04F4] = 0x04F5, + [0x04F6] = 0x04F7, + [0x04F8] = 0x04F9, + [0x04FA] = 0x04FB, + [0x04FC] = 0x04FD, + [0x04FE] = 0x04FF, + [0x0500] = 0x0501, + [0x0502] = 0x0503, + [0x0504] = 0x0505, + [0x0506] = 0x0507, + [0x0508] = 0x0509, + [0x050A] = 0x050B, + [0x050C] = 0x050D, + [0x050E] = 0x050F, + [0x0510] = 0x0511, + [0x0512] = 0x0513, + [0x0514] = 0x0515, + [0x0516] = 0x0517, + [0x0518] = 0x0519, + [0x051A] = 0x051B, + [0x051C] = 0x051D, + [0x051E] = 0x051F, + [0x0520] = 0x0521, + [0x0522] = 0x0523, + [0x0524] = 0x0525, + [0x0526] = 0x0527, + [0x0528] = 0x0529, + [0x052A] = 0x052B, + [0x052C] = 0x052D, + [0x052E] = 0x052F, + [0x0531] = 0x0561, + [0x0532] = 0x0562, + [0x0533] = 0x0563, + [0x0534] = 0x0564, + [0x0535] = 0x0565, + [0x0536] = 0x0566, + [0x0537] = 0x0567, + [0x0538] = 0x0568, + [0x0539] = 0x0569, + [0x053A] = 0x056A, + [0x053B] = 0x056B, + [0x053C] = 0x056C, + [0x053D] = 0x056D, + [0x053E] = 0x056E, + [0x053F] = 0x056F, + [0x0540] = 0x0570, + [0x0541] = 0x0571, + [0x0542] = 0x0572, + [0x0543] = 0x0573, + [0x0544] = 0x0574, + [0x0545] = 0x0575, + [0x0546] = 0x0576, + [0x0547] = 0x0577, + [0x0548] = 0x0578, + [0x0549] = 0x0579, + [0x054A] = 0x057A, + [0x054B] = 0x057B, + [0x054C] = 0x057C, + [0x054D] = 0x057D, + [0x054E] = 0x057E, + [0x054F] = 0x057F, + [0x0550] = 0x0580, + [0x0551] = 0x0581, + [0x0552] = 0x0582, + [0x0553] = 0x0583, + [0x0554] = 0x0584, + [0x0555] = 0x0585, + [0x0556] = 0x0586, + [0x0587] = {0x0565, 0x0582}, + [0x10A0] = 0x2D00, + [0x10A1] = 0x2D01, + [0x10A2] = 0x2D02, + [0x10A3] = 0x2D03, + [0x10A4] = 0x2D04, + [0x10A5] = 0x2D05, + [0x10A6] = 0x2D06, + [0x10A7] = 0x2D07, + [0x10A8] = 0x2D08, + [0x10A9] = 0x2D09, + [0x10AA] = 0x2D0A, + [0x10AB] = 0x2D0B, + [0x10AC] = 0x2D0C, + [0x10AD] = 0x2D0D, + [0x10AE] = 0x2D0E, + [0x10AF] = 0x2D0F, + [0x10B0] = 0x2D10, + [0x10B1] = 0x2D11, + [0x10B2] = 0x2D12, + [0x10B3] = 0x2D13, + [0x10B4] = 0x2D14, + [0x10B5] = 0x2D15, + [0x10B6] = 0x2D16, + [0x10B7] = 0x2D17, + [0x10B8] = 0x2D18, + [0x10B9] = 0x2D19, + [0x10BA] = 0x2D1A, + [0x10BB] = 0x2D1B, + [0x10BC] = 0x2D1C, + [0x10BD] = 0x2D1D, + [0x10BE] = 0x2D1E, + [0x10BF] = 0x2D1F, + [0x10C0] = 0x2D20, + [0x10C1] = 0x2D21, + [0x10C2] = 0x2D22, + [0x10C3] = 0x2D23, + [0x10C4] = 0x2D24, + [0x10C5] = 0x2D25, + [0x10C7] = 0x2D27, + [0x10CD] = 0x2D2D, + [0x13F8] = 0x13F0, + [0x13F9] = 0x13F1, + [0x13FA] = 0x13F2, + [0x13FB] = 0x13F3, + [0x13FC] = 0x13F4, + [0x13FD] = 0x13F5, + [0x1C80] = 0x0432, + [0x1C81] = 0x0434, + [0x1C82] = 0x043E, + [0x1C83] = 0x0441, + [0x1C84] = 0x0442, + [0x1C85] = 0x0442, + [0x1C86] = 0x044A, + [0x1C87] = 0x0463, + [0x1C88] = 0xA64B, + [0x1C90] = 0x10D0, + [0x1C91] = 0x10D1, + [0x1C92] = 0x10D2, + [0x1C93] = 0x10D3, + [0x1C94] = 0x10D4, + [0x1C95] = 0x10D5, + [0x1C96] = 0x10D6, + [0x1C97] = 0x10D7, + [0x1C98] = 0x10D8, + [0x1C99] = 0x10D9, + [0x1C9A] = 0x10DA, + [0x1C9B] = 0x10DB, + [0x1C9C] = 0x10DC, + [0x1C9D] = 0x10DD, + [0x1C9E] = 0x10DE, + [0x1C9F] = 0x10DF, + [0x1CA0] = 0x10E0, + [0x1CA1] = 0x10E1, + [0x1CA2] = 0x10E2, + [0x1CA3] = 0x10E3, + [0x1CA4] = 0x10E4, + [0x1CA5] = 0x10E5, + [0x1CA6] = 0x10E6, + [0x1CA7] = 0x10E7, + [0x1CA8] = 0x10E8, + [0x1CA9] = 0x10E9, + [0x1CAA] = 0x10EA, + [0x1CAB] = 0x10EB, + [0x1CAC] = 0x10EC, + [0x1CAD] = 0x10ED, + [0x1CAE] = 0x10EE, + [0x1CAF] = 0x10EF, + [0x1CB0] = 0x10F0, + [0x1CB1] = 0x10F1, + [0x1CB2] = 0x10F2, + [0x1CB3] = 0x10F3, + [0x1CB4] = 0x10F4, + [0x1CB5] = 0x10F5, + [0x1CB6] = 0x10F6, + [0x1CB7] = 0x10F7, + [0x1CB8] = 0x10F8, + [0x1CB9] = 0x10F9, + [0x1CBA] = 0x10FA, + [0x1CBD] = 0x10FD, + [0x1CBE] = 0x10FE, + [0x1CBF] = 0x10FF, + [0x1E00] = 0x1E01, + [0x1E02] = 0x1E03, + [0x1E04] = 0x1E05, + [0x1E06] = 0x1E07, + [0x1E08] = 0x1E09, + [0x1E0A] = 0x1E0B, + [0x1E0C] = 0x1E0D, + [0x1E0E] = 0x1E0F, + [0x1E10] = 0x1E11, + [0x1E12] = 0x1E13, + [0x1E14] = 0x1E15, + [0x1E16] = 0x1E17, + [0x1E18] = 0x1E19, + [0x1E1A] = 0x1E1B, + [0x1E1C] = 0x1E1D, + [0x1E1E] = 0x1E1F, + [0x1E20] = 0x1E21, + [0x1E22] = 0x1E23, + [0x1E24] = 0x1E25, + [0x1E26] = 0x1E27, + [0x1E28] = 0x1E29, + [0x1E2A] = 0x1E2B, + [0x1E2C] = 0x1E2D, + [0x1E2E] = 0x1E2F, + [0x1E30] = 0x1E31, + [0x1E32] = 0x1E33, + [0x1E34] = 0x1E35, + [0x1E36] = 0x1E37, + [0x1E38] = 0x1E39, + [0x1E3A] = 0x1E3B, + [0x1E3C] = 0x1E3D, + [0x1E3E] = 0x1E3F, + [0x1E40] = 0x1E41, + [0x1E42] = 0x1E43, + [0x1E44] = 0x1E45, + [0x1E46] = 0x1E47, + [0x1E48] = 0x1E49, + [0x1E4A] = 0x1E4B, + [0x1E4C] = 0x1E4D, + [0x1E4E] = 0x1E4F, + [0x1E50] = 0x1E51, + [0x1E52] = 0x1E53, + [0x1E54] = 0x1E55, + [0x1E56] = 0x1E57, + [0x1E58] = 0x1E59, + [0x1E5A] = 0x1E5B, + [0x1E5C] = 0x1E5D, + [0x1E5E] = 0x1E5F, + [0x1E60] = 0x1E61, + [0x1E62] = 0x1E63, + [0x1E64] = 0x1E65, + [0x1E66] = 0x1E67, + [0x1E68] = 0x1E69, + [0x1E6A] = 0x1E6B, + [0x1E6C] = 0x1E6D, + [0x1E6E] = 0x1E6F, + [0x1E70] = 0x1E71, + [0x1E72] = 0x1E73, + [0x1E74] = 0x1E75, + [0x1E76] = 0x1E77, + [0x1E78] = 0x1E79, + [0x1E7A] = 0x1E7B, + [0x1E7C] = 0x1E7D, + [0x1E7E] = 0x1E7F, + [0x1E80] = 0x1E81, + [0x1E82] = 0x1E83, + [0x1E84] = 0x1E85, + [0x1E86] = 0x1E87, + [0x1E88] = 0x1E89, + [0x1E8A] = 0x1E8B, + [0x1E8C] = 0x1E8D, + [0x1E8E] = 0x1E8F, + [0x1E90] = 0x1E91, + [0x1E92] = 0x1E93, + [0x1E94] = 0x1E95, + [0x1E96] = {0x0068, 0x0331}, + [0x1E97] = {0x0074, 0x0308}, + [0x1E98] = {0x0077, 0x030A}, + [0x1E99] = {0x0079, 0x030A}, + [0x1E9A] = {0x0061, 0x02BE}, + [0x1E9B] = 0x1E61, + [0x1E9E] = {0x0073, 0x0073}, + --1E9E; S;0x00DF, ; # LATIN CAPITAL LETTER SHARP S + [0x1EA0] = 0x1EA1, + [0x1EA2] = 0x1EA3, + [0x1EA4] = 0x1EA5, + [0x1EA6] = 0x1EA7, + [0x1EA8] = 0x1EA9, + [0x1EAA] = 0x1EAB, + [0x1EAC] = 0x1EAD, + [0x1EAE] = 0x1EAF, + [0x1EB0] = 0x1EB1, + [0x1EB2] = 0x1EB3, + [0x1EB4] = 0x1EB5, + [0x1EB6] = 0x1EB7, + [0x1EB8] = 0x1EB9, + [0x1EBA] = 0x1EBB, + [0x1EBC] = 0x1EBD, + [0x1EBE] = 0x1EBF, + [0x1EC0] = 0x1EC1, + [0x1EC2] = 0x1EC3, + [0x1EC4] = 0x1EC5, + [0x1EC6] = 0x1EC7, + [0x1EC8] = 0x1EC9, + [0x1ECA] = 0x1ECB, + [0x1ECC] = 0x1ECD, + [0x1ECE] = 0x1ECF, + [0x1ED0] = 0x1ED1, + [0x1ED2] = 0x1ED3, + [0x1ED4] = 0x1ED5, + [0x1ED6] = 0x1ED7, + [0x1ED8] = 0x1ED9, + [0x1EDA] = 0x1EDB, + [0x1EDC] = 0x1EDD, + [0x1EDE] = 0x1EDF, + [0x1EE0] = 0x1EE1, + [0x1EE2] = 0x1EE3, + [0x1EE4] = 0x1EE5, + [0x1EE6] = 0x1EE7, + [0x1EE8] = 0x1EE9, + [0x1EEA] = 0x1EEB, + [0x1EEC] = 0x1EED, + [0x1EEE] = 0x1EEF, + [0x1EF0] = 0x1EF1, + [0x1EF2] = 0x1EF3, + [0x1EF4] = 0x1EF5, + [0x1EF6] = 0x1EF7, + [0x1EF8] = 0x1EF9, + [0x1EFA] = 0x1EFB, + [0x1EFC] = 0x1EFD, + [0x1EFE] = 0x1EFF, + [0x1F08] = 0x1F00, + [0x1F09] = 0x1F01, + [0x1F0A] = 0x1F02, + [0x1F0B] = 0x1F03, + [0x1F0C] = 0x1F04, + [0x1F0D] = 0x1F05, + [0x1F0E] = 0x1F06, + [0x1F0F] = 0x1F07, + [0x1F18] = 0x1F10, + [0x1F19] = 0x1F11, + [0x1F1A] = 0x1F12, + [0x1F1B] = 0x1F13, + [0x1F1C] = 0x1F14, + [0x1F1D] = 0x1F15, + [0x1F28] = 0x1F20, + [0x1F29] = 0x1F21, + [0x1F2A] = 0x1F22, + [0x1F2B] = 0x1F23, + [0x1F2C] = 0x1F24, + [0x1F2D] = 0x1F25, + [0x1F2E] = 0x1F26, + [0x1F2F] = 0x1F27, + [0x1F38] = 0x1F30, + [0x1F39] = 0x1F31, + [0x1F3A] = 0x1F32, + [0x1F3B] = 0x1F33, + [0x1F3C] = 0x1F34, + [0x1F3D] = 0x1F35, + [0x1F3E] = 0x1F36, + [0x1F3F] = 0x1F37, + [0x1F48] = 0x1F40, + [0x1F49] = 0x1F41, + [0x1F4A] = 0x1F42, + [0x1F4B] = 0x1F43, + [0x1F4C] = 0x1F44, + [0x1F4D] = 0x1F45, + [0x1F50] = {0x03C5, 0x0313}, + [0x1F52] = {0x03C5, 0x0313, 0x0300}, + [0x1F54] = {0x03C5, 0x0313, 0x0301}, + [0x1F56] = {0x03C5, 0x0313, 0x0342}, + [0x1F59] = 0x1F51, + [0x1F5B] = 0x1F53, + [0x1F5D] = 0x1F55, + [0x1F5F] = 0x1F57, + [0x1F68] = 0x1F60, + [0x1F69] = 0x1F61, + [0x1F6A] = 0x1F62, + [0x1F6B] = 0x1F63, + [0x1F6C] = 0x1F64, + [0x1F6D] = 0x1F65, + [0x1F6E] = 0x1F66, + [0x1F6F] = 0x1F67, + [0x1F80] = {0x1F00, 0x03B9}, + [0x1F81] = {0x1F01, 0x03B9}, + [0x1F82] = {0x1F02, 0x03B9}, + [0x1F83] = {0x1F03, 0x03B9}, + [0x1F84] = {0x1F04, 0x03B9}, + [0x1F85] = {0x1F05, 0x03B9}, + [0x1F86] = {0x1F06, 0x03B9}, + [0x1F87] = {0x1F07, 0x03B9}, + [0x1F88] = {0x1F00, 0x03B9}, + --1F88; S;0x1F80, ; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI + [0x1F89] = {0x1F01, 0x03B9}, + --1F89; S;0x1F81, ; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI + [0x1F8A] = {0x1F02, 0x03B9}, + --1F8A; S;0x1F82, ; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI + [0x1F8B] = {0x1F03, 0x03B9}, + --1F8B; S;0x1F83, ; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI + [0x1F8C] = {0x1F04, 0x03B9}, + --1F8C; S;0x1F84, ; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI + [0x1F8D] = {0x1F05, 0x03B9}, + --1F8D; S;0x1F85, ; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI + [0x1F8E] = {0x1F06, 0x03B9}, + --1F8E; S;0x1F86, ; # GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + [0x1F8F] = {0x1F07, 0x03B9}, + --1F8F; S;0x1F87, ; # GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + [0x1F90] = {0x1F20, 0x03B9}, + [0x1F91] = {0x1F21, 0x03B9}, + [0x1F92] = {0x1F22, 0x03B9}, + [0x1F93] = {0x1F23, 0x03B9}, + [0x1F94] = {0x1F24, 0x03B9}, + [0x1F95] = {0x1F25, 0x03B9}, + [0x1F96] = {0x1F26, 0x03B9}, + [0x1F97] = {0x1F27, 0x03B9}, + [0x1F98] = {0x1F20, 0x03B9}, + --1F98; S;0x1F90, ; # GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI + [0x1F99] = {0x1F21, 0x03B9}, + --1F99; S;0x1F91, ; # GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI + [0x1F9A] = {0x1F22, 0x03B9}, + --1F9A; S;0x1F92, ; # GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI + [0x1F9B] = {0x1F23, 0x03B9}, + --1F9B; S;0x1F93, ; # GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI + [0x1F9C] = {0x1F24, 0x03B9}, + --1F9C; S;0x1F94, ; # GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI + [0x1F9D] = {0x1F25, 0x03B9}, + --1F9D; S;0x1F95, ; # GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI + [0x1F9E] = {0x1F26, 0x03B9}, + --1F9E; S;0x1F96, ; # GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + [0x1F9F] = {0x1F27, 0x03B9}, + --1F9F; S;0x1F97, ; # GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + [0x1FA0] = {0x1F60, 0x03B9}, + [0x1FA1] = {0x1F61, 0x03B9}, + [0x1FA2] = {0x1F62, 0x03B9}, + [0x1FA3] = {0x1F63, 0x03B9}, + [0x1FA4] = {0x1F64, 0x03B9}, + [0x1FA5] = {0x1F65, 0x03B9}, + [0x1FA6] = {0x1F66, 0x03B9}, + [0x1FA7] = {0x1F67, 0x03B9}, + [0x1FA8] = {0x1F60, 0x03B9}, + --1FA8; S;0x1FA0, ; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI + [0x1FA9] = {0x1F61, 0x03B9}, + --1FA9; S;0x1FA1, ; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI + [0x1FAA] = {0x1F62, 0x03B9}, + --1FAA; S;0x1FA2, ; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI + [0x1FAB] = {0x1F63, 0x03B9}, + --1FAB; S;0x1FA3, ; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI + [0x1FAC] = {0x1F64, 0x03B9}, + --1FAC; S;0x1FA4, ; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI + [0x1FAD] = {0x1F65, 0x03B9}, + --1FAD; S;0x1FA5, ; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI + [0x1FAE] = {0x1F66, 0x03B9}, + --1FAE; S;0x1FA6, ; # GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + [0x1FAF] = {0x1F67, 0x03B9}, + --1FAF; S;0x1FA7, ; # GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + [0x1FB2] = {0x1F70, 0x03B9}, + [0x1FB3] = {0x03B1, 0x03B9}, + [0x1FB4] = {0x03AC, 0x03B9}, + [0x1FB6] = {0x03B1, 0x0342}, + [0x1FB7] = {0x03B1, 0x0342, 0x03B9}, + [0x1FB8] = 0x1FB0, + [0x1FB9] = 0x1FB1, + [0x1FBA] = 0x1F70, + [0x1FBB] = 0x1F71, + [0x1FBC] = {0x03B1, 0x03B9}, + --1FBC; S;0x1FB3, ; # GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI + [0x1FBE] = 0x03B9, + [0x1FC2] = {0x1F74, 0x03B9}, + [0x1FC3] = {0x03B7, 0x03B9}, + [0x1FC4] = {0x03AE, 0x03B9}, + [0x1FC6] = {0x03B7, 0x0342}, + [0x1FC7] = {0x03B7, 0x0342, 0x03B9}, + [0x1FC8] = 0x1F72, + [0x1FC9] = 0x1F73, + [0x1FCA] = 0x1F74, + [0x1FCB] = 0x1F75, + [0x1FCC] = {0x03B7, 0x03B9}, + --1FCC; S;0x1FC3, ; # GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI + [0x1FD2] = {0x03B9, 0x0308, 0x0300}, + [0x1FD3] = {0x03B9, 0x0308, 0x0301}, + [0x1FD6] = {0x03B9, 0x0342}, + [0x1FD7] = {0x03B9, 0x0308, 0x0342}, + [0x1FD8] = 0x1FD0, + [0x1FD9] = 0x1FD1, + [0x1FDA] = 0x1F76, + [0x1FDB] = 0x1F77, + [0x1FE2] = {0x03C5, 0x0308, 0x0300}, + [0x1FE3] = {0x03C5, 0x0308, 0x0301}, + [0x1FE4] = {0x03C1, 0x0313}, + [0x1FE6] = {0x03C5, 0x0342}, + [0x1FE7] = {0x03C5, 0x0308, 0x0342}, + [0x1FE8] = 0x1FE0, + [0x1FE9] = 0x1FE1, + [0x1FEA] = 0x1F7A, + [0x1FEB] = 0x1F7B, + [0x1FEC] = 0x1FE5, + [0x1FF2] = {0x1F7C, 0x03B9}, + [0x1FF3] = {0x03C9, 0x03B9}, + [0x1FF4] = {0x03CE, 0x03B9}, + [0x1FF6] = {0x03C9, 0x0342}, + [0x1FF7] = {0x03C9, 0x0342, 0x03B9}, + [0x1FF8] = 0x1F78, + [0x1FF9] = 0x1F79, + [0x1FFA] = 0x1F7C, + [0x1FFB] = 0x1F7D, + [0x1FFC] = {0x03C9, 0x03B9}, + --1FFC; S;0x1FF3, ; # GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI + [0x2126] = 0x03C9, + [0x212A] = 0x006B, + [0x212B] = 0x00E5, + [0x2132] = 0x214E, + [0x2160] = 0x2170, + [0x2161] = 0x2171, + [0x2162] = 0x2172, + [0x2163] = 0x2173, + [0x2164] = 0x2174, + [0x2165] = 0x2175, + [0x2166] = 0x2176, + [0x2167] = 0x2177, + [0x2168] = 0x2178, + [0x2169] = 0x2179, + [0x216A] = 0x217A, + [0x216B] = 0x217B, + [0x216C] = 0x217C, + [0x216D] = 0x217D, + [0x216E] = 0x217E, + [0x216F] = 0x217F, + [0x2183] = 0x2184, + [0x24B6] = 0x24D0, + [0x24B7] = 0x24D1, + [0x24B8] = 0x24D2, + [0x24B9] = 0x24D3, + [0x24BA] = 0x24D4, + [0x24BB] = 0x24D5, + [0x24BC] = 0x24D6, + [0x24BD] = 0x24D7, + [0x24BE] = 0x24D8, + [0x24BF] = 0x24D9, + [0x24C0] = 0x24DA, + [0x24C1] = 0x24DB, + [0x24C2] = 0x24DC, + [0x24C3] = 0x24DD, + [0x24C4] = 0x24DE, + [0x24C5] = 0x24DF, + [0x24C6] = 0x24E0, + [0x24C7] = 0x24E1, + [0x24C8] = 0x24E2, + [0x24C9] = 0x24E3, + [0x24CA] = 0x24E4, + [0x24CB] = 0x24E5, + [0x24CC] = 0x24E6, + [0x24CD] = 0x24E7, + [0x24CE] = 0x24E8, + [0x24CF] = 0x24E9, + [0x2C00] = 0x2C30, + [0x2C01] = 0x2C31, + [0x2C02] = 0x2C32, + [0x2C03] = 0x2C33, + [0x2C04] = 0x2C34, + [0x2C05] = 0x2C35, + [0x2C06] = 0x2C36, + [0x2C07] = 0x2C37, + [0x2C08] = 0x2C38, + [0x2C09] = 0x2C39, + [0x2C0A] = 0x2C3A, + [0x2C0B] = 0x2C3B, + [0x2C0C] = 0x2C3C, + [0x2C0D] = 0x2C3D, + [0x2C0E] = 0x2C3E, + [0x2C0F] = 0x2C3F, + [0x2C10] = 0x2C40, + [0x2C11] = 0x2C41, + [0x2C12] = 0x2C42, + [0x2C13] = 0x2C43, + [0x2C14] = 0x2C44, + [0x2C15] = 0x2C45, + [0x2C16] = 0x2C46, + [0x2C17] = 0x2C47, + [0x2C18] = 0x2C48, + [0x2C19] = 0x2C49, + [0x2C1A] = 0x2C4A, + [0x2C1B] = 0x2C4B, + [0x2C1C] = 0x2C4C, + [0x2C1D] = 0x2C4D, + [0x2C1E] = 0x2C4E, + [0x2C1F] = 0x2C4F, + [0x2C20] = 0x2C50, + [0x2C21] = 0x2C51, + [0x2C22] = 0x2C52, + [0x2C23] = 0x2C53, + [0x2C24] = 0x2C54, + [0x2C25] = 0x2C55, + [0x2C26] = 0x2C56, + [0x2C27] = 0x2C57, + [0x2C28] = 0x2C58, + [0x2C29] = 0x2C59, + [0x2C2A] = 0x2C5A, + [0x2C2B] = 0x2C5B, + [0x2C2C] = 0x2C5C, + [0x2C2D] = 0x2C5D, + [0x2C2E] = 0x2C5E, + [0x2C60] = 0x2C61, + [0x2C62] = 0x026B, + [0x2C63] = 0x1D7D, + [0x2C64] = 0x027D, + [0x2C67] = 0x2C68, + [0x2C69] = 0x2C6A, + [0x2C6B] = 0x2C6C, + [0x2C6D] = 0x0251, + [0x2C6E] = 0x0271, + [0x2C6F] = 0x0250, + [0x2C70] = 0x0252, + [0x2C72] = 0x2C73, + [0x2C75] = 0x2C76, + [0x2C7E] = 0x023F, + [0x2C7F] = 0x0240, + [0x2C80] = 0x2C81, + [0x2C82] = 0x2C83, + [0x2C84] = 0x2C85, + [0x2C86] = 0x2C87, + [0x2C88] = 0x2C89, + [0x2C8A] = 0x2C8B, + [0x2C8C] = 0x2C8D, + [0x2C8E] = 0x2C8F, + [0x2C90] = 0x2C91, + [0x2C92] = 0x2C93, + [0x2C94] = 0x2C95, + [0x2C96] = 0x2C97, + [0x2C98] = 0x2C99, + [0x2C9A] = 0x2C9B, + [0x2C9C] = 0x2C9D, + [0x2C9E] = 0x2C9F, + [0x2CA0] = 0x2CA1, + [0x2CA2] = 0x2CA3, + [0x2CA4] = 0x2CA5, + [0x2CA6] = 0x2CA7, + [0x2CA8] = 0x2CA9, + [0x2CAA] = 0x2CAB, + [0x2CAC] = 0x2CAD, + [0x2CAE] = 0x2CAF, + [0x2CB0] = 0x2CB1, + [0x2CB2] = 0x2CB3, + [0x2CB4] = 0x2CB5, + [0x2CB6] = 0x2CB7, + [0x2CB8] = 0x2CB9, + [0x2CBA] = 0x2CBB, + [0x2CBC] = 0x2CBD, + [0x2CBE] = 0x2CBF, + [0x2CC0] = 0x2CC1, + [0x2CC2] = 0x2CC3, + [0x2CC4] = 0x2CC5, + [0x2CC6] = 0x2CC7, + [0x2CC8] = 0x2CC9, + [0x2CCA] = 0x2CCB, + [0x2CCC] = 0x2CCD, + [0x2CCE] = 0x2CCF, + [0x2CD0] = 0x2CD1, + [0x2CD2] = 0x2CD3, + [0x2CD4] = 0x2CD5, + [0x2CD6] = 0x2CD7, + [0x2CD8] = 0x2CD9, + [0x2CDA] = 0x2CDB, + [0x2CDC] = 0x2CDD, + [0x2CDE] = 0x2CDF, + [0x2CE0] = 0x2CE1, + [0x2CE2] = 0x2CE3, + [0x2CEB] = 0x2CEC, + [0x2CED] = 0x2CEE, + [0x2CF2] = 0x2CF3, + [0xA640] = 0xA641, + [0xA642] = 0xA643, + [0xA644] = 0xA645, + [0xA646] = 0xA647, + [0xA648] = 0xA649, + [0xA64A] = 0xA64B, + [0xA64C] = 0xA64D, + [0xA64E] = 0xA64F, + [0xA650] = 0xA651, + [0xA652] = 0xA653, + [0xA654] = 0xA655, + [0xA656] = 0xA657, + [0xA658] = 0xA659, + [0xA65A] = 0xA65B, + [0xA65C] = 0xA65D, + [0xA65E] = 0xA65F, + [0xA660] = 0xA661, + [0xA662] = 0xA663, + [0xA664] = 0xA665, + [0xA666] = 0xA667, + [0xA668] = 0xA669, + [0xA66A] = 0xA66B, + [0xA66C] = 0xA66D, + [0xA680] = 0xA681, + [0xA682] = 0xA683, + [0xA684] = 0xA685, + [0xA686] = 0xA687, + [0xA688] = 0xA689, + [0xA68A] = 0xA68B, + [0xA68C] = 0xA68D, + [0xA68E] = 0xA68F, + [0xA690] = 0xA691, + [0xA692] = 0xA693, + [0xA694] = 0xA695, + [0xA696] = 0xA697, + [0xA698] = 0xA699, + [0xA69A] = 0xA69B, + [0xA722] = 0xA723, + [0xA724] = 0xA725, + [0xA726] = 0xA727, + [0xA728] = 0xA729, + [0xA72A] = 0xA72B, + [0xA72C] = 0xA72D, + [0xA72E] = 0xA72F, + [0xA732] = 0xA733, + [0xA734] = 0xA735, + [0xA736] = 0xA737, + [0xA738] = 0xA739, + [0xA73A] = 0xA73B, + [0xA73C] = 0xA73D, + [0xA73E] = 0xA73F, + [0xA740] = 0xA741, + [0xA742] = 0xA743, + [0xA744] = 0xA745, + [0xA746] = 0xA747, + [0xA748] = 0xA749, + [0xA74A] = 0xA74B, + [0xA74C] = 0xA74D, + [0xA74E] = 0xA74F, + [0xA750] = 0xA751, + [0xA752] = 0xA753, + [0xA754] = 0xA755, + [0xA756] = 0xA757, + [0xA758] = 0xA759, + [0xA75A] = 0xA75B, + [0xA75C] = 0xA75D, + [0xA75E] = 0xA75F, + [0xA760] = 0xA761, + [0xA762] = 0xA763, + [0xA764] = 0xA765, + [0xA766] = 0xA767, + [0xA768] = 0xA769, + [0xA76A] = 0xA76B, + [0xA76C] = 0xA76D, + [0xA76E] = 0xA76F, + [0xA779] = 0xA77A, + [0xA77B] = 0xA77C, + [0xA77D] = 0x1D79, + [0xA77E] = 0xA77F, + [0xA780] = 0xA781, + [0xA782] = 0xA783, + [0xA784] = 0xA785, + [0xA786] = 0xA787, + [0xA78B] = 0xA78C, + [0xA78D] = 0x0265, + [0xA790] = 0xA791, + [0xA792] = 0xA793, + [0xA796] = 0xA797, + [0xA798] = 0xA799, + [0xA79A] = 0xA79B, + [0xA79C] = 0xA79D, + [0xA79E] = 0xA79F, + [0xA7A0] = 0xA7A1, + [0xA7A2] = 0xA7A3, + [0xA7A4] = 0xA7A5, + [0xA7A6] = 0xA7A7, + [0xA7A8] = 0xA7A9, + [0xA7AA] = 0x0266, + [0xA7AB] = 0x025C, + [0xA7AC] = 0x0261, + [0xA7AD] = 0x026C, + [0xA7AE] = 0x026A, + [0xA7B0] = 0x029E, + [0xA7B1] = 0x0287, + [0xA7B2] = 0x029D, + [0xA7B3] = 0xAB53, + [0xA7B4] = 0xA7B5, + [0xA7B6] = 0xA7B7, + [0xA7B8] = 0xA7B9, + [0xA7BA] = 0xA7BB, + [0xA7BC] = 0xA7BD, + [0xA7BE] = 0xA7BF, + [0xA7C2] = 0xA7C3, + [0xA7C4] = 0xA794, + [0xA7C5] = 0x0282, + [0xA7C6] = 0x1D8E, + [0xA7C7] = 0xA7C8, + [0xA7C9] = 0xA7CA, + [0xA7F5] = 0xA7F6, + [0xAB70] = 0x13A0, + [0xAB71] = 0x13A1, + [0xAB72] = 0x13A2, + [0xAB73] = 0x13A3, + [0xAB74] = 0x13A4, + [0xAB75] = 0x13A5, + [0xAB76] = 0x13A6, + [0xAB77] = 0x13A7, + [0xAB78] = 0x13A8, + [0xAB79] = 0x13A9, + [0xAB7A] = 0x13AA, + [0xAB7B] = 0x13AB, + [0xAB7C] = 0x13AC, + [0xAB7D] = 0x13AD, + [0xAB7E] = 0x13AE, + [0xAB7F] = 0x13AF, + [0xAB80] = 0x13B0, + [0xAB81] = 0x13B1, + [0xAB82] = 0x13B2, + [0xAB83] = 0x13B3, + [0xAB84] = 0x13B4, + [0xAB85] = 0x13B5, + [0xAB86] = 0x13B6, + [0xAB87] = 0x13B7, + [0xAB88] = 0x13B8, + [0xAB89] = 0x13B9, + [0xAB8A] = 0x13BA, + [0xAB8B] = 0x13BB, + [0xAB8C] = 0x13BC, + [0xAB8D] = 0x13BD, + [0xAB8E] = 0x13BE, + [0xAB8F] = 0x13BF, + [0xAB90] = 0x13C0, + [0xAB91] = 0x13C1, + [0xAB92] = 0x13C2, + [0xAB93] = 0x13C3, + [0xAB94] = 0x13C4, + [0xAB95] = 0x13C5, + [0xAB96] = 0x13C6, + [0xAB97] = 0x13C7, + [0xAB98] = 0x13C8, + [0xAB99] = 0x13C9, + [0xAB9A] = 0x13CA, + [0xAB9B] = 0x13CB, + [0xAB9C] = 0x13CC, + [0xAB9D] = 0x13CD, + [0xAB9E] = 0x13CE, + [0xAB9F] = 0x13CF, + [0xABA0] = 0x13D0, + [0xABA1] = 0x13D1, + [0xABA2] = 0x13D2, + [0xABA3] = 0x13D3, + [0xABA4] = 0x13D4, + [0xABA5] = 0x13D5, + [0xABA6] = 0x13D6, + [0xABA7] = 0x13D7, + [0xABA8] = 0x13D8, + [0xABA9] = 0x13D9, + [0xABAA] = 0x13DA, + [0xABAB] = 0x13DB, + [0xABAC] = 0x13DC, + [0xABAD] = 0x13DD, + [0xABAE] = 0x13DE, + [0xABAF] = 0x13DF, + [0xABB0] = 0x13E0, + [0xABB1] = 0x13E1, + [0xABB2] = 0x13E2, + [0xABB3] = 0x13E3, + [0xABB4] = 0x13E4, + [0xABB5] = 0x13E5, + [0xABB6] = 0x13E6, + [0xABB7] = 0x13E7, + [0xABB8] = 0x13E8, + [0xABB9] = 0x13E9, + [0xABBA] = 0x13EA, + [0xABBB] = 0x13EB, + [0xABBC] = 0x13EC, + [0xABBD] = 0x13ED, + [0xABBE] = 0x13EE, + [0xABBF] = 0x13EF, + [0xFB00] = {0x0066, 0x0066}, + [0xFB01] = {0x0066, 0x0069}, + [0xFB02] = {0x0066, 0x006C}, + [0xFB03] = {0x0066, 0x0066, 0x0069}, + [0xFB04] = {0x0066, 0x0066, 0x006C}, + [0xFB05] = {0x0073, 0x0074}, + [0xFB06] = {0x0073, 0x0074}, + [0xFB13] = {0x0574, 0x0576}, + [0xFB14] = {0x0574, 0x0565}, + [0xFB15] = {0x0574, 0x056B}, + [0xFB16] = {0x057E, 0x0576}, + [0xFB17] = {0x0574, 0x056D}, + [0xFF21] = 0xFF41, + [0xFF22] = 0xFF42, + [0xFF23] = 0xFF43, + [0xFF24] = 0xFF44, + [0xFF25] = 0xFF45, + [0xFF26] = 0xFF46, + [0xFF27] = 0xFF47, + [0xFF28] = 0xFF48, + [0xFF29] = 0xFF49, + [0xFF2A] = 0xFF4A, + [0xFF2B] = 0xFF4B, + [0xFF2C] = 0xFF4C, + [0xFF2D] = 0xFF4D, + [0xFF2E] = 0xFF4E, + [0xFF2F] = 0xFF4F, + [0xFF30] = 0xFF50, + [0xFF31] = 0xFF51, + [0xFF32] = 0xFF52, + [0xFF33] = 0xFF53, + [0xFF34] = 0xFF54, + [0xFF35] = 0xFF55, + [0xFF36] = 0xFF56, + [0xFF37] = 0xFF57, + [0xFF38] = 0xFF58, + [0xFF39] = 0xFF59, + [0xFF3A] = 0xFF5A, + [0x10400] = 0x10428, + [0x10401] = 0x10429, + [0x10402] = 0x1042A, + [0x10403] = 0x1042B, + [0x10404] = 0x1042C, + [0x10405] = 0x1042D, + [0x10406] = 0x1042E, + [0x10407] = 0x1042F, + [0x10408] = 0x10430, + [0x10409] = 0x10431, + [0x1040A] = 0x10432, + [0x1040B] = 0x10433, + [0x1040C] = 0x10434, + [0x1040D] = 0x10435, + [0x1040E] = 0x10436, + [0x1040F] = 0x10437, + [0x10410] = 0x10438, + [0x10411] = 0x10439, + [0x10412] = 0x1043A, + [0x10413] = 0x1043B, + [0x10414] = 0x1043C, + [0x10415] = 0x1043D, + [0x10416] = 0x1043E, + [0x10417] = 0x1043F, + [0x10418] = 0x10440, + [0x10419] = 0x10441, + [0x1041A] = 0x10442, + [0x1041B] = 0x10443, + [0x1041C] = 0x10444, + [0x1041D] = 0x10445, + [0x1041E] = 0x10446, + [0x1041F] = 0x10447, + [0x10420] = 0x10448, + [0x10421] = 0x10449, + [0x10422] = 0x1044A, + [0x10423] = 0x1044B, + [0x10424] = 0x1044C, + [0x10425] = 0x1044D, + [0x10426] = 0x1044E, + [0x10427] = 0x1044F, + [0x104B0] = 0x104D8, + [0x104B1] = 0x104D9, + [0x104B2] = 0x104DA, + [0x104B3] = 0x104DB, + [0x104B4] = 0x104DC, + [0x104B5] = 0x104DD, + [0x104B6] = 0x104DE, + [0x104B7] = 0x104DF, + [0x104B8] = 0x104E0, + [0x104B9] = 0x104E1, + [0x104BA] = 0x104E2, + [0x104BB] = 0x104E3, + [0x104BC] = 0x104E4, + [0x104BD] = 0x104E5, + [0x104BE] = 0x104E6, + [0x104BF] = 0x104E7, + [0x104C0] = 0x104E8, + [0x104C1] = 0x104E9, + [0x104C2] = 0x104EA, + [0x104C3] = 0x104EB, + [0x104C4] = 0x104EC, + [0x104C5] = 0x104ED, + [0x104C6] = 0x104EE, + [0x104C7] = 0x104EF, + [0x104C8] = 0x104F0, + [0x104C9] = 0x104F1, + [0x104CA] = 0x104F2, + [0x104CB] = 0x104F3, + [0x104CC] = 0x104F4, + [0x104CD] = 0x104F5, + [0x104CE] = 0x104F6, + [0x104CF] = 0x104F7, + [0x104D0] = 0x104F8, + [0x104D1] = 0x104F9, + [0x104D2] = 0x104FA, + [0x104D3] = 0x104FB, + [0x10C80] = 0x10CC0, + [0x10C81] = 0x10CC1, + [0x10C82] = 0x10CC2, + [0x10C83] = 0x10CC3, + [0x10C84] = 0x10CC4, + [0x10C85] = 0x10CC5, + [0x10C86] = 0x10CC6, + [0x10C87] = 0x10CC7, + [0x10C88] = 0x10CC8, + [0x10C89] = 0x10CC9, + [0x10C8A] = 0x10CCA, + [0x10C8B] = 0x10CCB, + [0x10C8C] = 0x10CCC, + [0x10C8D] = 0x10CCD, + [0x10C8E] = 0x10CCE, + [0x10C8F] = 0x10CCF, + [0x10C90] = 0x10CD0, + [0x10C91] = 0x10CD1, + [0x10C92] = 0x10CD2, + [0x10C93] = 0x10CD3, + [0x10C94] = 0x10CD4, + [0x10C95] = 0x10CD5, + [0x10C96] = 0x10CD6, + [0x10C97] = 0x10CD7, + [0x10C98] = 0x10CD8, + [0x10C99] = 0x10CD9, + [0x10C9A] = 0x10CDA, + [0x10C9B] = 0x10CDB, + [0x10C9C] = 0x10CDC, + [0x10C9D] = 0x10CDD, + [0x10C9E] = 0x10CDE, + [0x10C9F] = 0x10CDF, + [0x10CA0] = 0x10CE0, + [0x10CA1] = 0x10CE1, + [0x10CA2] = 0x10CE2, + [0x10CA3] = 0x10CE3, + [0x10CA4] = 0x10CE4, + [0x10CA5] = 0x10CE5, + [0x10CA6] = 0x10CE6, + [0x10CA7] = 0x10CE7, + [0x10CA8] = 0x10CE8, + [0x10CA9] = 0x10CE9, + [0x10CAA] = 0x10CEA, + [0x10CAB] = 0x10CEB, + [0x10CAC] = 0x10CEC, + [0x10CAD] = 0x10CED, + [0x10CAE] = 0x10CEE, + [0x10CAF] = 0x10CEF, + [0x10CB0] = 0x10CF0, + [0x10CB1] = 0x10CF1, + [0x10CB2] = 0x10CF2, + [0x118A0] = 0x118C0, + [0x118A1] = 0x118C1, + [0x118A2] = 0x118C2, + [0x118A3] = 0x118C3, + [0x118A4] = 0x118C4, + [0x118A5] = 0x118C5, + [0x118A6] = 0x118C6, + [0x118A7] = 0x118C7, + [0x118A8] = 0x118C8, + [0x118A9] = 0x118C9, + [0x118AA] = 0x118CA, + [0x118AB] = 0x118CB, + [0x118AC] = 0x118CC, + [0x118AD] = 0x118CD, + [0x118AE] = 0x118CE, + [0x118AF] = 0x118CF, + [0x118B0] = 0x118D0, + [0x118B1] = 0x118D1, + [0x118B2] = 0x118D2, + [0x118B3] = 0x118D3, + [0x118B4] = 0x118D4, + [0x118B5] = 0x118D5, + [0x118B6] = 0x118D6, + [0x118B7] = 0x118D7, + [0x118B8] = 0x118D8, + [0x118B9] = 0x118D9, + [0x118BA] = 0x118DA, + [0x118BB] = 0x118DB, + [0x118BC] = 0x118DC, + [0x118BD] = 0x118DD, + [0x118BE] = 0x118DE, + [0x118BF] = 0x118DF, + [0x16E40] = 0x16E60, + [0x16E41] = 0x16E61, + [0x16E42] = 0x16E62, + [0x16E43] = 0x16E63, + [0x16E44] = 0x16E64, + [0x16E45] = 0x16E65, + [0x16E46] = 0x16E66, + [0x16E47] = 0x16E67, + [0x16E48] = 0x16E68, + [0x16E49] = 0x16E69, + [0x16E4A] = 0x16E6A, + [0x16E4B] = 0x16E6B, + [0x16E4C] = 0x16E6C, + [0x16E4D] = 0x16E6D, + [0x16E4E] = 0x16E6E, + [0x16E4F] = 0x16E6F, + [0x16E50] = 0x16E70, + [0x16E51] = 0x16E71, + [0x16E52] = 0x16E72, + [0x16E53] = 0x16E73, + [0x16E54] = 0x16E74, + [0x16E55] = 0x16E75, + [0x16E56] = 0x16E76, + [0x16E57] = 0x16E77, + [0x16E58] = 0x16E78, + [0x16E59] = 0x16E79, + [0x16E5A] = 0x16E7A, + [0x16E5B] = 0x16E7B, + [0x16E5C] = 0x16E7C, + [0x16E5D] = 0x16E7D, + [0x16E5E] = 0x16E7E, + [0x16E5F] = 0x16E7F, + [0x1E900] = 0x1E922, + [0x1E901] = 0x1E923, + [0x1E902] = 0x1E924, + [0x1E903] = 0x1E925, + [0x1E904] = 0x1E926, + [0x1E905] = 0x1E927, + [0x1E906] = 0x1E928, + [0x1E907] = 0x1E929, + [0x1E908] = 0x1E92A, + [0x1E909] = 0x1E92B, + [0x1E90A] = 0x1E92C, + [0x1E90B] = 0x1E92D, + [0x1E90C] = 0x1E92E, + [0x1E90D] = 0x1E92F, + [0x1E90E] = 0x1E930, + [0x1E90F] = 0x1E931, + [0x1E910] = 0x1E932, + [0x1E911] = 0x1E933, + [0x1E912] = 0x1E934, + [0x1E913] = 0x1E935, + [0x1E914] = 0x1E936, + [0x1E915] = 0x1E937, + [0x1E916] = 0x1E938, + [0x1E917] = 0x1E939, + [0x1E918] = 0x1E93A, + [0x1E919] = 0x1E93B, + [0x1E91A] = 0x1E93C, + [0x1E91B] = 0x1E93D, + [0x1E91C] = 0x1E93E, + [0x1E91D] = 0x1E93F, + [0x1E91E] = 0x1E940, + [0x1E91F] = 0x1E941, + [0x1E920] = 0x1E942, + [0x1E921] = 0x1E943, +} +for k,v in pairs(utf8_case_conv_utl) do utf8_case_conv_ltu[v] = k end + +return UTFString \ No newline at end of file diff --git a/Capy64/Assets/bios.lua b/Capy64/Assets/bios.lua new file mode 100644 index 0000000..e2d9330 --- /dev/null +++ b/Capy64/Assets/bios.lua @@ -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 \ No newline at end of file diff --git a/Capy64/Assets/font.ttf b/Capy64/Assets/font.ttf new file mode 100644 index 0000000..efad9d2 Binary files /dev/null and b/Capy64/Assets/font.ttf differ diff --git a/Capy64/BIOS/Bios.cs b/Capy64/BIOS/Bios.cs new file mode 100644 index 0000000..00be48f --- /dev/null +++ b/Capy64/BIOS/Bios.cs @@ -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(_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; + } +} diff --git a/Capy64/BIOS/PanicScreen.cs b/Capy64/BIOS/PanicScreen.cs new file mode 100644 index 0000000..b2c3077 --- /dev/null +++ b/Capy64/BIOS/PanicScreen.cs @@ -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); + } +} diff --git a/Capy64/BIOS/RuntimeInputEvents.cs b/Capy64/BIOS/RuntimeInputEvents.cs new file mode 100644 index 0000000..eb7279f --- /dev/null +++ b/Capy64/BIOS/RuntimeInputEvents.cs @@ -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, + }); + } +} diff --git a/Capy64/Capy64.cs b/Capy64/Capy64.cs new file mode 100644 index 0000000..477e95c --- /dev/null +++ b/Capy64/Capy64.cs @@ -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 NativePlugins { get; private set; } + public IList 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 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(); + + 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); + } +} \ No newline at end of file diff --git a/Capy64/Capy64.csproj b/Capy64/Capy64.csproj new file mode 100644 index 0000000..e0e3a05 --- /dev/null +++ b/Capy64/Capy64.csproj @@ -0,0 +1,46 @@ + + + Exe + net7.0 + Major + false + false + + + app.manifest + Icon.ico + true + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/Capy64/Content/Content.mgcb b/Capy64/Content/Content.mgcb new file mode 100644 index 0000000..ddc4c36 --- /dev/null +++ b/Capy64/Content/Content.mgcb @@ -0,0 +1,15 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + diff --git a/Capy64/Core/Audio.cs b/Capy64/Core/Audio.cs new file mode 100644 index 0000000..aeadc5f --- /dev/null +++ b/Capy64/Core/Audio.cs @@ -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; + } + } + +} diff --git a/Capy64/Core/Drawing.cs b/Capy64/Core/Drawing.cs new file mode 100644 index 0000000..a5be972 --- /dev/null +++ b/Capy64/Core/Drawing.cs @@ -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 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 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(); + } +} diff --git a/Capy64/Eventing/EventEmitter.cs b/Capy64/Eventing/EventEmitter.cs new file mode 100644 index 0000000..acaf5ea --- /dev/null +++ b/Capy64/Eventing/EventEmitter.cs @@ -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 OnMouseDown; + public event EventHandler OnMouseUp; + public event EventHandler OnMouseMove; + public event EventHandler OnMouseWheel; + + // Keyboard events + public event EventHandler OnKeyDown; + public event EventHandler OnKeyUp; + public event EventHandler OnChar; + + // Functional events + public event EventHandler 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); + } + } +} diff --git a/Capy64/Eventing/Events/CharEvent.cs b/Capy64/Eventing/Events/CharEvent.cs new file mode 100644 index 0000000..d7d3a9f --- /dev/null +++ b/Capy64/Eventing/Events/CharEvent.cs @@ -0,0 +1,8 @@ +using System; + +namespace Capy64.Eventing.Events; + +public class CharEvent : EventArgs +{ + public char Character { get; set; } +} diff --git a/Capy64/Eventing/Events/KeyEvent.cs b/Capy64/Eventing/Events/KeyEvent.cs new file mode 100644 index 0000000..205bab6 --- /dev/null +++ b/Capy64/Eventing/Events/KeyEvent.cs @@ -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; } +} diff --git a/Capy64/Eventing/Events/MouseButtonEvent.cs b/Capy64/Eventing/Events/MouseButtonEvent.cs new file mode 100644 index 0000000..c5448ec --- /dev/null +++ b/Capy64/Eventing/Events/MouseButtonEvent.cs @@ -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; } +} diff --git a/Capy64/Eventing/Events/MouseMoveEvent.cs b/Capy64/Eventing/Events/MouseMoveEvent.cs new file mode 100644 index 0000000..d8e92e7 --- /dev/null +++ b/Capy64/Eventing/Events/MouseMoveEvent.cs @@ -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; } +} diff --git a/Capy64/Eventing/Events/MouseWheelEvent.cs b/Capy64/Eventing/Events/MouseWheelEvent.cs new file mode 100644 index 0000000..b70137d --- /dev/null +++ b/Capy64/Eventing/Events/MouseWheelEvent.cs @@ -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; } +} diff --git a/Capy64/Eventing/Events/TickEvent.cs b/Capy64/Eventing/Events/TickEvent.cs new file mode 100644 index 0000000..6d81b78 --- /dev/null +++ b/Capy64/Eventing/Events/TickEvent.cs @@ -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; } +} diff --git a/Capy64/Eventing/InputManager.cs b/Capy64/Eventing/InputManager.cs new file mode 100644 index 0000000..1606981 --- /dev/null +++ b/Capy64/Eventing/InputManager.cs @@ -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 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 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(); + } +} diff --git a/Capy64/Extensions/Bindings/SDL2.cs b/Capy64/Extensions/Bindings/SDL2.cs new file mode 100644 index 0000000..357b016 --- /dev/null +++ b/Capy64/Extensions/Bindings/SDL2.cs @@ -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); +} diff --git a/Capy64/Extensions/GameWindowExtensions.cs b/Capy64/Extensions/GameWindowExtensions.cs new file mode 100644 index 0000000..976beba --- /dev/null +++ b/Capy64/Extensions/GameWindowExtensions.cs @@ -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 +{ + /// + /// + /// + [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); + } +} diff --git a/Capy64/IGame.cs b/Capy64/IGame.cs new file mode 100644 index 0000000..224a874 --- /dev/null +++ b/Capy64/IGame.cs @@ -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 NativePlugins { get; } + IList 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 Exiting; + void Run(); + void Exit(); +} diff --git a/Capy64/Icon.bmp b/Capy64/Icon.bmp new file mode 100644 index 0000000..2b48165 Binary files /dev/null and b/Capy64/Icon.bmp differ diff --git a/Capy64/Icon.ico b/Capy64/Icon.ico new file mode 100644 index 0000000..7d9dec1 Binary files /dev/null and b/Capy64/Icon.ico differ diff --git a/Capy64/LuaRuntime/Constants.cs b/Capy64/LuaRuntime/Constants.cs new file mode 100644 index 0000000..054a7e0 --- /dev/null +++ b/Capy64/LuaRuntime/Constants.cs @@ -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; +} diff --git a/Capy64/LuaRuntime/Extensions/Libraries.cs b/Capy64/LuaRuntime/Extensions/Libraries.cs new file mode 100644 index 0000000..90071af --- /dev/null +++ b/Capy64/LuaRuntime/Extensions/Libraries.cs @@ -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); + } + } +} diff --git a/Capy64/LuaRuntime/Extensions/NativeLibraries.cs b/Capy64/LuaRuntime/Extensions/NativeLibraries.cs new file mode 100644 index 0000000..ff30b78 --- /dev/null +++ b/Capy64/LuaRuntime/Extensions/NativeLibraries.cs @@ -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); + } +} diff --git a/Capy64/LuaRuntime/Extensions/Utils.cs b/Capy64/LuaRuntime/Extensions/Utils.cs new file mode 100644 index 0000000..1c5d558 --- /dev/null +++ b/Capy64/LuaRuntime/Extensions/Utils.cs @@ -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(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); + } + } +} diff --git a/Capy64/LuaRuntime/Libraries/FileSystem.cs b/Capy64/LuaRuntime/Libraries/FileSystem.cs new file mode 100644 index 0000000..9d60f5b --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/FileSystem.cs @@ -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(); + 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(); + 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(); + + 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>(); + 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; + } + +} diff --git a/Capy64/LuaRuntime/Libraries/Graphics.cs b/Capy64/LuaRuntime/Libraries/Graphics.cs new file mode 100644 index 0000000..8ba51b7 --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/Graphics.cs @@ -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 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; + } +} diff --git a/Capy64/LuaRuntime/Libraries/HTTP.cs b/Capy64/LuaRuntime/Libraries/HTTP.cs new file mode 100644 index 0000000..3073bea --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/HTTP.cs @@ -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) + { + } +} diff --git a/Capy64/LuaRuntime/Libraries/OS.cs b/Capy64/LuaRuntime/Libraries/OS.cs new file mode 100644 index 0000000..9ae59ea --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/OS.cs @@ -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; + } +} diff --git a/Capy64/LuaRuntime/Libraries/Screen.cs b/Capy64/LuaRuntime/Libraries/Screen.cs new file mode 100644 index 0000000..48f56df --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/Screen.cs @@ -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; + } + +} diff --git a/Capy64/LuaRuntime/Libraries/Term.cs b/Capy64/LuaRuntime/Libraries/Term.cs new file mode 100644 index 0000000..396bceb --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/Term.cs @@ -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; + } +} diff --git a/Capy64/LuaRuntime/Libraries/Timer.cs b/Capy64/LuaRuntime/Libraries/Timer.cs new file mode 100644 index 0000000..c39aaa7 --- /dev/null +++ b/Capy64/LuaRuntime/Libraries/Timer.cs @@ -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; + } +} diff --git a/Capy64/LuaRuntime/LuaEvent.cs b/Capy64/LuaRuntime/LuaEvent.cs new file mode 100644 index 0000000..caf1ea3 --- /dev/null +++ b/Capy64/LuaRuntime/LuaEvent.cs @@ -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; } +} diff --git a/Capy64/LuaRuntime/LuaException.cs b/Capy64/LuaRuntime/LuaException.cs new file mode 100644 index 0000000..6afa5d7 --- /dev/null +++ b/Capy64/LuaRuntime/LuaException.cs @@ -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) + { + } +} diff --git a/Capy64/LuaRuntime/Runtime.cs b/Capy64/LuaRuntime/Runtime.cs new file mode 100644 index 0000000..76aa801 --- /dev/null +++ b/Capy64/LuaRuntime/Runtime.cs @@ -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 eventQueue = new(new LuaEvent[] + { + new() + { + Name = "init", + Parameters = Array.Empty() + } + }); + + private readonly Lua Parent; + public Lua Thread { get; private set; } + private string[] filters = Array.Empty(); + 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 }); + } + + /// + /// Resume the Lua thread + /// + /// Whether it yielded + 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(); + 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(); + } +} diff --git a/Capy64/LuaRuntime/Sandbox.cs b/Capy64/LuaRuntime/Sandbox.cs new file mode 100644 index 0000000..84b3223 --- /dev/null +++ b/Capy64/LuaRuntime/Sandbox.cs @@ -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("--------------"); + } + +} \ No newline at end of file diff --git a/Capy64/PluginManager/PluginLoadContext.cs b/Capy64/PluginManager/PluginLoadContext.cs new file mode 100644 index 0000000..e571602 --- /dev/null +++ b/Capy64/PluginManager/PluginLoadContext.cs @@ -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; + } + } +} diff --git a/Capy64/PluginManager/PluginLoader.cs b/Capy64/PluginManager/PluginLoader.cs new file mode 100644 index 0000000..4f75319 --- /dev/null +++ b/Capy64/PluginManager/PluginLoader.cs @@ -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 LoadAllPlugins(string pluginsPath, IServiceProvider provider) + { + if (!Directory.Exists(pluginsPath)) + Directory.CreateDirectory(pluginsPath); + + var plugins = new List(); + 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; + } +} diff --git a/Capy64/Program.cs b/Capy64/Program.cs new file mode 100644 index 0000000..02ca9f5 --- /dev/null +++ b/Capy64/Program.cs @@ -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(game); + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/Capy64/Properties/launchSettings.json b/Capy64/Properties/launchSettings.json new file mode 100644 index 0000000..d11e4a2 --- /dev/null +++ b/Capy64/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "distributionName": "" + }, + "Capy64": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Capy64/Utils.cs b/Capy64/Utils.cs new file mode 100644 index 0000000..a3b546f --- /dev/null +++ b/Capy64/Utils.cs @@ -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); + } +} diff --git a/Capy64/Worker.cs b/Capy64/Worker.cs new file mode 100644 index 0000000..18a583d --- /dev/null +++ b/Capy64/Worker.cs @@ -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() + { + } +} \ No newline at end of file diff --git a/Capy64/app.manifest b/Capy64/app.manifest new file mode 100644 index 0000000..35dbe91 --- /dev/null +++ b/Capy64/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + +