Merge branch 'main' of https://github.com/26F-Studio/Techmino into mod-patch

This commit is contained in:
SweetSea
2024-11-10 14:48:54 +07:00
165 changed files with 2168 additions and 1265 deletions

103
Zframework/clipboard.lua Normal file
View File

@@ -0,0 +1,103 @@
local function _sanitize(content)
if type(content)=='boolean' then
content=content and 'true' or 'false'
end
if type(content)=='nil' then
content=''
end
if type(content)=='number' then
content=tostring(content)
end
if type(content)~='string' then
MES.new('error',"Invalid content type!")
MES.traceback()
return ''
end
return content
end
if SYSTEM~='Web' then
local get=love.system.getClipboardText
local set=love.system.setClipboardText
return {
get=function() return get() or '' end,
set=function(content) set(_sanitize(content)) end,
setFreshInterval=NULL,
_update=NULL,
}
end
if WEB_COMPAT_MODE then
local _clipboardBuffer=''
return {
get=function()
JS.newPromiseRequest(
JS.stringFunc(
[[
window.navigator.clipboard
.readText()
.then((text) => _$_(text))
.catch((e) => {
console.warn(e);
_$_('');
});
]]
),
function(data) _clipboardBuffer=data end,
function() _clipboardBuffer='' end,
3,
'getClipboardText'
)
if TASK.lock('clipboard_compat_interval',2.6) then
_clipboardBuffer=''
MES.new('warn',"Web-Compat mode, paste again to confirm",2.6)
end
return _clipboardBuffer
end,
set=function(str)
JS.callJS(JS.stringFunc(
[[
window.navigator.clipboard
.writeText('%s')
.then(() => console.log('Copied to clipboard'))
.catch((e) => console.warn(e));
]],
_sanitize(str)
))
end,
setFreshInterval=NULL,
_update=NULL,
}
end
local getCHN=love.thread.getChannel('CLIP_get')
local setCHN=love.thread.getChannel('CLIP_set')
local trigCHN=love.thread.getChannel('CLIP_trig')
local clipboard_thread=love.thread.newThread('Zframework/clipboard_thread.lua')
local isStarted,errorMessage=clipboard_thread:start()
if not isStarted then
MES.new("error",errorMessage,26)
end
local freshInterval=1
local timer=-.626
return {
get=function() return getCHN:peek() or '' end,
set=function(content) setCHN:push(_sanitize(content)) end,
setFreshInterval=function(val)
freshInterval=val
end,
_update=function(dt)
timer=timer+dt
if timer>freshInterval then
if isStarted and not clipboard_thread:isRunning() then
MES.new("warn",clipboard_thread:getError(),26)
isStarted=false
end
trigCHN:push(timer)
timer=0
end
end,
}

View File

@@ -0,0 +1,48 @@
local getCHN=love.thread.getChannel('CLIP_get')
local setCHN=love.thread.getChannel('CLIP_set')
local trigCHN=love.thread.getChannel('CLIP_trig')
JS=require'Zframework.js'
local sleep=require'love.timer'.sleep
local retrieving=false
while true do
if trigCHN:getCount()>0 then
local dt=trigCHN:pop()
if setCHN:getCount()>0 then
while setCHN:getCount()>1 do setCHN:pop() end
-- Set Clipboard
JS.callJS(JS.stringFunc(
[[
window.navigator.clipboard
.writeText('%s')
.then(() => console.log('Copied to clipboard'))
.catch((e) => console.warn(e));
]],
setCHN:pop()
))
end
-- Get Clipboard
if not retrieving then
JS.newPromiseRequest(
JS.stringFunc[[
window.navigator.clipboard
.readText()
.then((text) => _$_(text))
.catch((e)=>{});
]],
function(data)
while getCHN:getCount()>0 do getCHN:pop() end
getCHN:push(data)
retrieving=false
end,
function() retrieving=false end,
1,
'getClipboardText'
)
retrieving=true
end
JS.retrieveData(dt)
end
sleep(.001)
end

View File

@@ -1,5 +1,5 @@
local sendCHN=love.thread.getChannel('inputChannel')
local recvCHN=love.thread.getChannel('outputChannel')
local sendCHN=love.thread.getChannel('HTTP_inputChannel')
local recvCHN=love.thread.getChannel('HTTP_outputChannel')
local threads={}
local threadCount=0
@@ -9,11 +9,15 @@ local threadCode=[[
local http=require'socket.http'
local ltn12=require'ltn12'
local sendCHN=love.thread.getChannel('inputChannel')
local recvCHN=love.thread.getChannel('outputChannel')
local sendCHN=love.thread.getChannel('HTTP_inputChannel')
local recvCHN=love.thread.getChannel('HTTP_outputChannel')
local sleep=require'love.timer'.sleep
while true do
local arg=sendCHN:demand()
-- local arg=sendCHN:demand()
-- Warning: workaround for love.js
while sendCHN:getCount()==0 do sleep(.0626) end
local arg=sendCHN:pop()
if arg._destroy then
recvCHN:push{

View File

@@ -3,8 +3,16 @@
NONE={}function NULL() end PAPER=love.graphics.newCanvas(1,1)
EDITING=""
LOADED=false
---@type 'Windows'|'Android'|'Linux'|'iOS'|'macOS'|'Web'
SYSTEM=love.system.getOS()
if SYSTEM=='OS X' then SYSTEM='macOS' end
WEB_COMPAT_MODE=false
if SYSTEM=='OS X' then
SYSTEM='macOS'
elseif SYSTEM=='Web' then
WEB_COMPAT_MODE=not love.thread.newThread('\n'):start()
print('Web compatible mode: ', WEB_COMPAT_MODE)
end
-- Bit module
local success
@@ -72,6 +80,7 @@ do
end
-- Love-based modules (basic)
CLIPBOARD= require'Zframework.clipboard'
HTTP= require'Zframework.http'
WS= require'Zframework.websocket'
FILE= require'Zframework.file'
@@ -94,6 +103,10 @@ IMG= require'Zframework.image'
BGM= require'Zframework.bgm'
VOC= require'Zframework.voice'
if SYSTEM=='Web' then
JS=require'Zframework.js'
end
local ms,kb=love.mouse,love.keyboard
local KBisDown=kb.isDown
@@ -174,6 +187,7 @@ local function updatePowerInfo()
gc_pop()
gc.setCanvas()
end
-------------------------------------------------------------
local lastX,lastY=0,0-- Last click pos
local function _updateMousePos(x,y,dx,dy)
@@ -423,38 +437,38 @@ local dPadToKey={
start='return',
back='escape',
}
function love.joystickadded(JS)
function love.joystickadded(joystick)
table.insert(jsState,{
_id=JS:getID(),
_jsObj=JS,
_id=joystick:getID(),
_jsObj=joystick,
leftx=0,lefty=0,
rightx=0,righty=0,
triggerleft=0,triggerright=0
})
MES.new('info',"Joystick added")
end
function love.joystickremoved(JS)
function love.joystickremoved(joystick)
for i=1,#jsState do
if jsState[i]._jsObj==JS then
if jsState[i]._jsObj==joystick then
for j=1,#gamePadKeys do
if JS:isGamepadDown(gamePadKeys[j]) then
love.gamepadreleased(JS,gamePadKeys[j])
if joystick:isGamepadDown(gamePadKeys[j]) then
love.gamepadreleased(joystick,gamePadKeys[j])
end
end
love.gamepadaxis(JS,'leftx',0)
love.gamepadaxis(JS,'lefty',0)
love.gamepadaxis(JS,'rightx',0)
love.gamepadaxis(JS,'righty',0)
love.gamepadaxis(JS,'triggerleft',-1)
love.gamepadaxis(JS,'triggerright',-1)
love.gamepadaxis(joystick,'leftx',0)
love.gamepadaxis(joystick,'lefty',0)
love.gamepadaxis(joystick,'rightx',0)
love.gamepadaxis(joystick,'righty',0)
love.gamepadaxis(joystick,'triggerleft',-1)
love.gamepadaxis(joystick,'triggerright',-1)
MES.new('info',"Joystick removed")
table.remove(jsState,i)
break
end
end
end
function love.gamepadaxis(JS,axis,val)
if jsState[1] and JS==jsState[1]._jsObj then
function love.gamepadaxis(joystick,axis,val)
if jsState[1] and joystick==jsState[1]._jsObj then
local js=jsState[1]
if axis=='leftx' or axis=='lefty' or axis=='rightx' or axis=='righty' then
local newVal=-- range: [0,1]
@@ -463,14 +477,14 @@ function love.gamepadaxis(JS,axis,val)
0
if newVal~=js[axis] then
if js[axis]==-1 then
love.gamepadreleased(JS,jsAxisEventName[axis][1])
love.gamepadreleased(joystick,jsAxisEventName[axis][1])
elseif js[axis]~=0 then
love.gamepadreleased(JS,jsAxisEventName[axis][2])
love.gamepadreleased(joystick,jsAxisEventName[axis][2])
end
if newVal==-1 then
love.gamepadpressed(JS,jsAxisEventName[axis][1])
love.gamepadpressed(joystick,jsAxisEventName[axis][1])
elseif newVal==1 then
love.gamepadpressed(JS,jsAxisEventName[axis][2])
love.gamepadpressed(joystick,jsAxisEventName[axis][2])
end
js[axis]=newVal
end
@@ -478,9 +492,9 @@ function love.gamepadaxis(JS,axis,val)
local newVal=val>.3 and 1 or 0-- range: [0,1]
if newVal~=js[axis] then
if newVal==1 then
love.gamepadpressed(JS,jsAxisEventName[axis])
love.gamepadpressed(joystick,jsAxisEventName[axis])
else
love.gamepadreleased(JS,jsAxisEventName[axis])
love.gamepadreleased(joystick,jsAxisEventName[axis])
end
js[axis]=newVal
end
@@ -720,6 +734,10 @@ function love.run()
-- UPDATE
STEP()
if SYSTEM == 'Web' then
JS.retrieveData(dt)
CLIPBOARD._update(dt)
end
if mouseShow then mouse_update(dt) end
if next(jsState) then gp_update(jsState[1],dt) end
VOC.update()

142
Zframework/js.lua Normal file
View File

@@ -0,0 +1,142 @@
local __requestQueue={}
local _requestCount=0
local _Request={
command="",
currentTime=0,
timeOut=2,
id='0',
}
local __defaultErrorFunction=nil
local isDebugActive=false
local JS={}
function JS.callJS(funcToCall)
print("callJavascriptFunction "..funcToCall)
end
--You can pass a set of commands here and, it is a syntactic sugar for executing many commands inside callJS, as it only calls a function
--If you pass arguments to the func beyond the string, it will perform automatically string.format
--Return statement is possible inside this structure
--This will return a string containing a function to be called by JS.callJS
function JS.stringFunc(str,...)
str="(function(){"..str.."})()"
if (#arg>0) then
str=str:format(unpack(arg))
end
str=str:gsub("[\n\t]","")
return str
end
--The call will store in the webDB the return value from the function passed it timeouts
local function retrieveJS(funcToCall,filename)
--Used for retrieveData function
JS.callJS(("FS.writeFile('%s/%s',%s);"):format(love.filesystem.getSaveDirectory(),filename,funcToCall))
end
--Call JS.newRequest instead
function _Request:new(isPromise,command,onDataLoaded,onError,timeout,id)
local obj={}
setmetatable(obj,self)
obj.command=command
obj.onError=onError or __defaultErrorFunction
if not isPromise then
retrieveJS(command,self.filename)
else
JS.callJS(command)
end
obj.onDataLoaded=onDataLoaded
obj.timeOut=(timeout==nil) and obj.timeOut or timeout
obj.id=id
obj.filename="__temp"..id
function obj:getData()
--Try to read from webdb
if love.filesystem.getInfo(self.filename) then
return love.filesystem.read(self.filename)
end
end
function obj:purgeData()
--Data must be purged for not allowing old data to be retrieved
love.filesystem.remove(self.filename)
end
function obj:update(dt)
self.timeOut=self.timeOut-dt
local retData=self:getData()
if ((retData~=nil and retData~="nil") or self.timeOut<=0) then
if (retData~=nil and retData:match("ERROR")==nil) then
if isDebugActive then
print("Data has been retrieved "..retData)
end
self.onDataLoaded(retData)
else
self.onError(self.id,retData)
end
self:purgeData()
return false
else
return true
end
end
return obj
end
--Place this function on love.update and set it to return if it returns false (This API is synchronous)
function JS.retrieveData(dt)
local isRetrieving=#__requestQueue~=0
local deadRequests={}
for i=1,#__requestQueue do
local isUpdating=__requestQueue[i]:update(dt)
if not isUpdating then
table.insert(deadRequests,i)
end
end
for i=1,#deadRequests do
if (isDebugActive) then
print("Request died: "..deadRequests[i])
end
table.remove(__requestQueue,deadRequests[i])
end
return isRetrieving
end
--May only be used for functions that don't return a promise
function JS.newRequest(funcToCall,onDataLoaded,onError,timeout,optionalId)
table.insert(__requestQueue,_Request:new(false,funcToCall,onDataLoaded,onError,timeout or 5,optionalId or _requestCount))
end
--This function can be handled manually (in JS code)
--How to: add the function call when your events resolve: FS.writeFile("Put love.filesystem.getSaveDirectory here", "Pass a string here (NUMBER DONT WORK"))
--Or it can be handled by Lua, it auto sets your data if you write the following command:
-- _$_(yourStringOrFunctionHere)
function JS.newPromiseRequest(funcToCall,onDataLoaded,onError,timeout,optionalId)
optionalId=optionalId or _requestCount
funcToCall=funcToCall:gsub("_$_%(","FS.writeFile('"..love.filesystem.getSaveDirectory().."/__temp"..optionalId.."', ")
table.insert(__requestQueue,_Request:new(true,funcToCall,onDataLoaded,onError,timeout or 5,optionalId))
end
--It receives the ID from ther request
--Don't try printing the request.command, as it will execute the javascript command
function JS.setDefaultErrorFunction(func)
__defaultErrorFunction=func
end
JS.setDefaultErrorFunction(function(id,error)
if (isDebugActive) then
local msg="Data could not be loaded for id:'"..id.."'"
if (error) then
msg=msg.."\nError: "..error
end
print(msg)
end
end)
JS.callJS(JS.stringFunc("__getWebDB('%s');","__LuaJSDB"))
return JS

View File

@@ -140,7 +140,7 @@ function profile.switch()
switch=not switch
if not switch then
profile.stop()
love.system.setClipboardText(profile.report())
CLIPBOARD.set(profile.report())
profile.reset()
return false
else

View File

@@ -131,9 +131,8 @@ function STRING.time_short(t)
-- floor seconds
timeUnits[#timeUnits]=floorint(timeUnits[#timeUnits])
local outputStr=''
for i=1,#timeUnits do
if timeUnits>0 then
if timeUnits[i]>0 then
return timeUnits[i]..timeLetters[i]..' '..timeUnits[i+1]..timeLetters[i+1]
end
end
@@ -190,7 +189,7 @@ do-- functions to shorted big numbers
function STRING.bigInt(t)
if t<1000 then
return tostring(t)
elseif t~=1e999 then
elseif t~=1/0 then
local e=floorint(lg(t)/3)
return(t/10^(e*3))..units[e+1]
else

View File

@@ -8,8 +8,6 @@ local path=''
local type=type
local timer=love.timer.getTime
local TRD=love.thread.newThread("\n")
local TRD_isRunning=TRD.isRunning
local WS={}
local wsList=setmetatable({},{
@@ -151,7 +149,7 @@ function WS.update(dt)
local time=timer()
for name,ws in next,wsList do
if ws.real and ws.status~='dead' then
if TRD_isRunning(ws.thread) then
if ws.thread:isRunning() then
if ws.triggerCHN:getCount()==0 then
ws.triggerCHN:push(0)
end

View File

@@ -1,3 +1,4 @@
---@type love.Channel,love.Channel,love.Channel
local triggerCHN,sendCHN,readCHN=...
local CHN_demand,CHN_getCount=triggerCHN.demand,triggerCHN.getCount
@@ -5,16 +6,20 @@ local CHN_push,CHN_pop=triggerCHN.push,triggerCHN.pop
local SOCK=require'socket'.tcp()
local JSON=require'Zframework.json'
local sleep=require'love.timer'.sleep
do-- Connect
local host=CHN_demand(sendCHN)
local port=CHN_demand(sendCHN)
local path=CHN_demand(sendCHN)
local head=CHN_demand(sendCHN)
local timeout=CHN_demand(sendCHN)
-- Warning: workaround for love.js, used to use CHN_demand instead
while CHN_getCount(sendCHN)<5 do sleep(.0626) end
local host=CHN_pop(sendCHN)
local port=CHN_pop(sendCHN)
local path=CHN_pop(sendCHN)
local head=CHN_pop(sendCHN)
local timeout=CHN_pop(sendCHN)
SOCK:settimeout(timeout)
local res,err=SOCK:connect(host,port)
-- print('C0',res,err)
assert(res,err)
-- WebSocket handshake
@@ -31,6 +36,7 @@ do-- Connect
-- First line of HTTP
res,err=SOCK:receive('*l')
-- print('C',res,err)
assert(res,err)
local code,ctLen
code=res:find(' ')
@@ -39,22 +45,28 @@ do-- Connect
-- Get body length from headers and remove headers
repeat
res,err=SOCK:receive('*l')
-- print('H',res,err)
assert(res,err)
if not ctLen and res:find('length') then
ctLen=tonumber(res:match('%d+'))
if not ctLen and res:find('content-length') then
ctLen=tonumber(res:match('%d+')) or 0
end
until res==''
-- Result
if code=='101' then
CHN_push(readCHN,'success')
end
-- Content(?)
if ctLen then
if code=='101' then
CHN_push(readCHN,'success')
else
res,err=SOCK:receive(ctLen)
res,err=SOCK:receive(ctLen)
-- print('R',res,err)
if code~='101' then
res=JSON.decode(assert(res,err))
error((code or "XXX")..":"..(res and res.reason or "Server Error"))
end
end
SOCK:settimeout(0)
end
@@ -136,10 +148,10 @@ local readThread=coroutine.wrap(function()
assert(res,err)
length=shl(byte(res,1),8)+byte(res,2)
elseif length==127 then
local lenData
lenData,err=_receive(SOCK,8)
-- 'res' is 'lenData' here
res,err=_receive(SOCK,8)
assert(res,err)
local _,_,_,_,_5,_6,_7,_8=byte(lenData,1,8)
local _,_,_,_,_5,_6,_7,_8=byte(res,1,8)
length=shl(_5,24)+shl(_6,16)+shl(_7,8)+_8
end
res,err=_receive(SOCK,length)
@@ -158,12 +170,14 @@ local readThread=coroutine.wrap(function()
lBuffer=lBuffer..res
if fin then
CHN_push(readCHN,lBuffer)
-- print('M',lBuffer)
lBuffer=""
end
else
CHN_push(readCHN,op)
if fin then
CHN_push(readCHN,res)
-- print('S',res)
lBuffer=""
else
lBuffer=res
@@ -176,7 +190,8 @@ end)
local success,err
while true do-- Running
CHN_demand(triggerCHN)
while CHN_getCount(triggerCHN)==0 do sleep(.0626) end
CHN_pop(triggerCHN)
success,err=pcall(sendThread)
if not success or err then break end
success,err=pcall(readThread)

View File

@@ -15,6 +15,7 @@ local timer=love.timer.getTime
local next=next
local floor,ceil=math.floor,math.ceil
local max,min=math.max,math.min
local match=string.match
local sub,ins,rem=string.sub,table.insert,table.remove
local xOy=SCR.xOy
local FONT=FONT
@@ -142,13 +143,21 @@ local button={
type='button',
mustHaveText=true,
ATV=0,-- Activating time(0~8)
textAlreadyWrapped=false,-- Text already wrapped? (Managed by :setObject, can be override, this will be true if obj has a '\n')
}
function button:reset()
self.ATV=0
end
function button:setObject(obj)
if type(obj)=='string' or type(obj)=='number' then
self.obj=gc.newText(FONT.get(self.font,self.fType),obj)
if match(obj,"\n") then
self.textAlreadyWrapped=true
self.obj=gc.newText(FONT.get(self.font,self.fType))
self.obj:addf(obj,self.w-self.edge*2,(self.align=='L' and 'left') or (self.align=='R' and 'right') or 'center')
else
self.textAlreadyWrapped=false
self.obj=gc.newText(FONT.get(self.font,self.fType),obj)
end
elseif obj then
self.obj=obj
end
@@ -194,16 +203,7 @@ function button:draw()
local ox,oy=obj:getWidth()*.5,obj:getHeight()*.5
local y0=y+h*.5
gc_setColor(1,1,1,.2+ATV*.05)
if self.align=='M' then
local x0=x+w*.5
local kx=obj:type()=='Text' and min(w/ox/2,1) or 1
gc_draw(obj,x0-1,y0-1,nil,kx,1,ox,oy)
gc_draw(obj,x0-1,y0+1,nil,kx,1,ox,oy)
gc_draw(obj,x0+1,y0-1,nil,kx,1,ox,oy)
gc_draw(obj,x0+1,y0+1,nil,kx,1,ox,oy)
gc_setColor(r*.55,g*.55,b*.55)
gc_draw(obj,x0,y0,nil,kx,1,ox,oy)
elseif self.align=='L' then
if self.align=='L' or self.textAlreadyWrapped then
local edge=self.edge
gc_draw(obj,x+edge-1,y0-1-oy)
gc_draw(obj,x+edge-1,y0+1-oy)
@@ -219,6 +219,15 @@ function button:draw()
gc_draw(obj,x0+1,y0+1-oy)
gc_setColor(r*.55,g*.55,b*.55)
gc_draw(obj,x0,y0-oy)
else--if self.align=='M' then
local x0=x+w*.5
local kx=obj:type()=='Text' and min(w/ox/2,1) or 1
gc_draw(obj,x0-1,y0-1,nil,kx,1,ox,oy)
gc_draw(obj,x0-1,y0+1,nil,kx,1,ox,oy)
gc_draw(obj,x0+1,y0-1,nil,kx,1,ox,oy)
gc_draw(obj,x0+1,y0+1,nil,kx,1,ox,oy)
gc_setColor(r*.55,g*.55,b*.55)
gc_draw(obj,x0,y0,nil,kx,1,ox,oy)
end
end
function button:getInfo()
@@ -290,13 +299,21 @@ local key={
type='key',
mustHaveText=true,
ATV=0,-- Activating time(0~4)
textAlreadyWrapped=false,---See button.setObject (line 146)
}
function key:reset()
self.ATV=0
end
function key:setObject(obj)
if type(obj)=='string' or type(obj)=='number' then
self.obj=gc.newText(FONT.get(self.font,self.fType),obj)
if match(obj,"\n") then
self.textAlreadyWrapped=true
self.obj=gc.newText(FONT.get(self.font,self.fType))
self.obj:addf(obj,self.w-self.edge*2,(self.align=='L' and 'left') or (self.align=='R' and 'right') or 'center')
else
self.textAlreadyWrapped=false
self.obj=gc.newText(FONT.get(self.font,self.fType),obj)
end
elseif obj then
self.obj=obj
end
@@ -354,14 +371,15 @@ function key:draw()
-- Drawable
local obj=self.obj
local ox,oy=obj:getWidth()*.5,obj:getHeight()*.5
gc_setColor(r,g,b)
if align=='M' then
local kx=obj:type()=='Text' and min(w/ox/2,1) or 1
gc_draw(obj,x+w*.5,y+h*.5,nil,kx,1,ox,oy)
elseif align=='L' then
gc_draw(obj,x+self.edge,y-oy+h*.5)
if align=='L' or self.textAlreadyWrapped then
gc_draw(obj,x+self.edge,y+h*.5-oy)
elseif align=='R' then
gc_draw(obj,x+w-self.edge-ox*2,y-oy+h*.5)
else--if align=='M' then
local kx=obj:type()=='Text' and min(w/ox/2,1) or 1
gc_draw(obj,x+w*.5,y+h*.5,nil,kx,1,ox,oy)
end
end
function key:getInfo()
@@ -1382,10 +1400,13 @@ function WIDGET.setLang(widgetText)
t=W.name or "##"
W.color=COLOR.dV
end
if type(t)=='string' and W.font then
t=gc.newText(FONT.get(W.font),t)
if type(W.setObject)=='function' then
W:setObject(t)
elseif type(t)=='string' and W.font then
W.obj=gc.newText(FONT.get(W.font or 30),t)
else
W.obj=t
end
W.obj=t
end
end
end