429 lines
12 KiB
Lua

--[[
MIT License
Copyright (c) 2019 Ryan Ward
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sub-license, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local multi = require("multi.compat.love2d")
function multi:canSystemThread()
return true
end
function multi:getPlatform()
return "love2d"
end
multi.integration={}
multi.integration.love2d={}
multi.integration.love2d.ThreadBase=[[
tab={...}
__THREADID__=table.remove(tab,1)
__THREADNAME__=table.remove(tab,1)
THREAD_ID=table.remove(tab,1)
require("love.filesystem")
require("love.system")
require("love.timer")
require("love.image")
local multi = require("multi")
GLOBAL={}
isMainThread=false
setmetatable(GLOBAL,{
__index=function(t,k)
__sync__()
return __proxy__[k]
end,
__newindex=function(t,k,v)
__sync__()
__proxy__[k]=v
if type(v)=="userdata" then
__MainChan__:push(v)
else
__MainChan__:push("SYNC "..type(v).." "..k.." "..resolveData(v))
end
end,
})
function __sync__()
local data=__mythread__:pop()
while data do
love.timer.sleep(.01)
if type(data)=="string" then
local cmd,tp,name,d=data:match("(%S-) (%S-) (%S-) (.+)")
if name=="__DIEPLZ"..__THREADID__.."__" then
error("Thread: "..__THREADID__.." has been stopped!")
end
if cmd=="SYNC" then
__proxy__[name]=resolveType(tp,d)
end
else
__proxy__[name]=data
end
data=__mythread__:pop()
end
end
function ToStr(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if type(name) == "string" then
tmp = tmp .. "[\""..name.."\"] = "
else
tmp = tmp .. "["..(name or "").."] = "
end
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and " " or "")
for k, v in pairs(val) do
tmp = tmp .. ToStr(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and " " or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
elseif type(val) == "function" then
tmp = tmp .. "loadDump([===["..dump(val).."]===])"
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
function resolveType(tp,d)
if tp=="number" then
return tonumber(d)
elseif tp=="bool" then
return (d=="true")
elseif tp=="function" then
return loadDump("[==["..d.."]==]")
elseif tp=="table" then
return loadstring("return "..d)()
elseif tp=="nil" then
return nil
else
return d
end
end
function resolveData(v)
local data=""
if type(v)=="table" then
return ToStr(v)
elseif type(v)=="function" then
return dump(v)
elseif type(v)=="string" or type(v)=="number" or type(v)=="bool" or type(v)=="nil" then
return tostring(v)
end
return data
end
sThread={}
local function randomString(n)
local c=os.clock()
local a=0
while os.clock()<c+.1 do
a=a+1 -- each cpu has a different load... Doing this allows up to make unique seeds for the random string
end
math.randomseed(a)
local str = ''
local strings = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','1','2','3','4','5','6','7','8','9','0','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}
for i=1,n do
str = str..''..strings[math.random(1,#strings)]
end
return str
end
__proxy__={} -- this is where the actual globals will live
__MainChan__=love.thread.getChannel("__MainChan__")
__mythreadname__=randomString(16)
__mythread__=love.thread.getChannel(__mythreadname__)
__MainChan__:push("NEWTHREAD "..__mythreadname__.." | |")
function loadDump(d)
local s={}
for p in d:gmatch("(%d-)\\") do
s[#s+1]=string.char(tonumber(p))
end
return loadstring(table.concat(s))
end
function dump(func)
local code,t={},string.dump(func)
for i=1,#t do
code[#code+1]=string.byte(t:sub(i,i)).."\\"
end
return table.concat(code)
end
function sThread.getName()
return __THREADNAME__
end
function sThread.getID()
return THREAD_ID
end
function sThread.kill()
error("Thread was killed!")
end
function sThread.set(name,val)
GLOBAL[name]=val
end
function sThread.get(name)
return GLOBAL[name]
end
function sThread.waitFor(name)
repeat __sync__() until GLOBAL[name]
return GLOBAL[name]
end
function sThread.getCores()
return love.system.getProcessorCount()
end
function sThread.sleep(n)
love.timer.sleep(n)
end
function sThread.hold(n)
repeat __sync__() until n()
end
updater=multi:newUpdater()
updater:OnUpdate(__sync__)
func=loadDump([=[INSERT_USER_CODE]=])(unpack(tab))
multi:mainloop()
]]
GLOBAL={} -- Allow main thread to interact with these objects as well
_G.THREAD_ID = 0
__proxy__={}
setmetatable(GLOBAL,{
__index=function(t,k)
return __proxy__[k]
end,
__newindex=function(t,k,v)
__proxy__[k]=v
for i=1,#__channels__ do
if type(v)=="userdata" then
__channels__[i]:push(v)
else
__channels__[i]:push("SYNC "..type(v).." "..k.." "..resolveData(v))
end
end
end,
})
THREAD={} -- Allow main thread to interact with these objects as well
multi.integration.love2d.mainChannel=love.thread.getChannel("__MainChan__")
isMainThread=true
multi.SystemThreads = {}
function THREAD.getName()
return __THREADNAME__
end
function THREAD.getID()
return THREAD_ID
end
function ToStr(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if type(name) == "string" then
tmp = tmp .. "[\""..name.."\"] = "
else
tmp = tmp .. "["..(name or "").."] = "
end
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and " " or "")
for k, v in pairs(val) do
tmp = tmp .. ToStr(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and " " or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
elseif type(val) == "function" then
tmp = tmp .. "loadDump([===["..dump(val).."]===])"
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
function resolveType(tp,d)
if tp=="number" then
return tonumber(d)
elseif tp=="bool" then
return (d=="true")
elseif tp=="function" then
return loadDump("[==["..d.."]==]")
elseif tp=="table" then
return loadstring("return "..d)()
elseif tp=="nil" then
return nil
else
return d
end
end
function resolveData(v)
local data=""
if type(v)=="table" then
return ToStr(v)
elseif type(v)=="function" then
return dump(v)
elseif type(v)=="string" or type(v)=="number" or type(v)=="bool" or type(v)=="nil" then
return tostring(v)
end
return data
end
function loadDump(d)
local s={}
for p in d:gmatch("(%d-)\\") do
s[#s+1]=string.char(tonumber(p))
end
return loadstring(table.concat(s))
end
function dump(func)
local code,t={},string.dump(func)
for i=1,#t do
code[#code+1]=string.byte(t:sub(i,i)).."\\"
end
return table.concat(code)
end
local function randomString(n)
local str = ''
local strings = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','1','2','3','4','5','6','7','8','9','0','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}
for i=1,n do
str = str..''..strings[math.random(1,#strings)]
end
return str
end
local count = 1
local livingThreads = {}
function multi:newSystemThread(name,func,...) -- the main method
multi.InitSystemThreadErrorHandler()
local c={}
c.name=name
c.Name = name
c.ID=c.name.."<ID|"..randomString(8)..">"
c.Id=count
count = count + 1
livingThreads[count] = {true,name}
c.thread=love.thread.newThread(multi.integration.love2d.ThreadBase:gsub("INSERT_USER_CODE",dump(func)))
c.thread:start(c.ID,c.name,THREAD_ID,...)
function c:kill()
multi.integration.GLOBAL["__DIEPLZ"..self.ID.."__"]="__DIEPLZ"..self.ID.."__"
end
return c
end
function love.threaderror( thread, errorstr )
multi.OnError:Fire(thread,errorstr)
multi.print("Error in systemThread: "..tostring(thread)..": "..errorstr)
end
local THREAD={}
function THREAD.set(name,val)
GLOBAL[name]=val
end
function THREAD.get(name)
return GLOBAL[name]
end
function THREAD.waitFor(name)
repeat multi:uManager() until GLOBAL[name]
return GLOBAL[name]
end
function THREAD.getCores()
return love.system.getProcessorCount()
end
function THREAD.sleep(n)
love.timer.sleep(n)
end
function THREAD.hold(n)
repeat multi:uManager() until n()
end
__channels__={}
multi.integration.GLOBAL=GLOBAL
multi.integration.THREAD=THREAD
updater=multi:newLoop(function(self)
local data=multi.integration.love2d.mainChannel:pop()
while data do
if type(data)=="string" then
local cmd,tp,name,d=data:match("(%S-) (%S-) (%S-) (.+)")
if cmd=="SYNC" then
__proxy__[name]=resolveType(tp,d)
for i=1,#__channels__ do
-- send data to other threads
if type(v)=="userdata" then
__channels__[i]:push(v)
else
__channels__[i]:push("SYNC "..tp.." "..name.." "..d)
end
end
elseif cmd=="NEWTHREAD" then
__channels__[#__channels__+1]=love.thread.getChannel(tp)
for k,v in pairs(__proxy__) do -- sync the global with each new thread
if type(v)=="userdata" then
__channels__[#__channels__]:push(v)
else
__channels__[#__channels__]:push("SYNC "..type(v).." "..k.." "..resolveData(v))
end
end
end
else
__proxy__[name]=data
end
data=multi.integration.love2d.mainChannel:pop()
end
end)
multi.OnSystemThreadDied = multi:newConnection()
local started = false
function multi.InitSystemThreadErrorHandler()
if started==true then return end
started = true
multi:newThread("ThreadErrorHandler",function()
local threads = multi.SystemThreads
while true do
thread.sleep(.5) -- switching states often takes a huge hit on performance. half a second to tell me there is an error is good enough.
for i=#threads,1,-1 do
local v,err,t=threads[i].thread:join(.001)
if err then
if err:find("Thread was killed!") then
livingThreads[threads[i].Id] = {false,threads[i].Name}
multi.OnSystemThreadDied:Fire(threads[i].Id)
GLOBAL["__THREADS__"]=livingThreads
table.remove(threads,i)
else
threads[i].OnError:Fire(threads[i],err,"Error in systemThread: '"..threads[i].name.."' <"..err..">")
livingThreads[threads[i].Id] = {false,threads[i].Name}
multi.OnSystemThreadDied:Fire(threads[i].Id)
GLOBAL["__THREADS__"]=livingThreads
table.remove(threads,i)
end
end
end
end
end)
end
require("multi.integration.shared")
multi.print("Integrated Love2d!")
return {
init=function(t)
if t then
if t.threadNamespace then
multi.integration.THREADNAME=t.threadNamespace
multi.integration.love2d.ThreadBase:gsub("sThread",t.threadNamespace)
end
if t.globalNamespace then
multi.integration.GLOBALNAME=t.globalNamespace
multi.integration.love2d.ThreadBase:gsub("GLOBAL",t.globalNamespace)
end
end
return GLOBAL,THREAD
end
}