├── LICENSE ├── README.md ├── consolewrapper.js ├── globalizeFS.js ├── js.lua └── webdb.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marcelo Silva Nascimento Mancini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Love.js-Api-Player 2 | Created as a binder between love.js and javascript, it is made to be able to call javascript functions from lua code 3 | 4 | # How it works 5 | - It wraps the window.console logging function, and it eval's the string passed as a JS function, it still mantains the correct functionality from the old console, unless passing the key string to it, it will not log into the devtools console, as it should just execute it 6 | - **BEWARE**: There are some sites that doesn't let you wrap their console, if you're not able doing, the API won't work 7 | 8 | # How to 9 | - Download js.lua file 10 | - Add the following lua function to your main.lua 11 | ```lua 12 | require 'js' 13 | ``` 14 | - Convert your project to [love.js](https://github.com/TannerRogalsky/love.js), all thanks to TannerRogalsky 15 | - Add the **``** to the index.html file created by the love.js conversion 16 | - After that, for a simple JS call, where you don't expect any return value, call: 17 | ```lua 18 | JS.callJS('kongregate.stats.submit("Score", 1000);') 19 | ``` 20 | 21 | ## Executing Javascript code block 22 | It is possible to execute js code block, but you will need to call another function with JS.callJS: 23 | ```lua 24 | JS.callJS(JS.stringFunc( 25 | [[ 26 | var test = 5; 27 | test+= 15; 28 | console.log(test); 29 | ]] 30 | )) 31 | ``` 32 | Code blocks also supports formatted characters for easy passing parameters inside your code: 33 | ```lua 34 | JS.callJS(JS.stringFunc( 35 | [[ 36 | console.log("stringFromJS "+ "%s"); 37 | ]] 38 | , "stringFromLua") 39 | ``` 40 | Please, note that code blocks doesn't support Javascript comments 41 | 42 | - However, if you do wish to retrieve the value from the JS Api Call, it is a bit more complicated, you will need to use the full extent of js.lua: 43 | 44 | ## Setting up for data retrieve 45 | 46 | 1. After importing js.lua file, call `JS.newRequest(strApiToCallInJS, closureOnSuccess)` 47 | 1. **strApiToCallInJS**: a string of the api you want to call 48 | 2. **closureOnSuccess**: A function to call when the data arrives 49 | 2. For retrieving your data, you must set it as a closure, kongregate has one api for getting the player username: kongregate is: kongregate.services.getUsername(), so let's try showing how we can get the data: 50 | ```lua 51 | gUsername = "" 52 | JS.newRequest('kongregate.services.getUsername()', 53 | function(data) 54 | gUsername = data 55 | end) 56 | ``` 57 | 1. If you wish to retrieve data from a code block, simply return from the code block 58 | ```lua 59 | JS.newRequest(JS.stringFunc( 60 | [[ 61 | var test = 5; 62 | return test + 15; 63 | ]] 64 | )) 65 | ``` 66 | 67 | 3. This will make the request active, and it will store in the indexed database, for actually completing the request, you must choose if you want it to be sync or assync, in this example, I'm going to show the sync one: 68 | 1. In **love.update**, make it the first line: 69 | ```lua 70 | if(JS.retrieveData(dt)) then 71 | return 72 | end 73 | ``` 74 | 2. This function will return wether if it is still trying to retrieve or not, so, returning will make the game don't update 75 | 3. After that, you're set up for using the lib 76 | 77 | ## Handling promises for data retrieving 78 | This lib can be quite powerful if you understood how to use it, for handling promises, there is 2 ways possible 79 | ## 1. From Javascript code 80 | If you wish to call a Javascript function that will set the data for you, you will need to call a function that will receive(or set) your save path and your id: 81 | ```lua 82 | JS.newPromiseRequest("myJsFunc('"..love.filesystem.getSaveDirectory().."', '"..myLuaId.."');", onDataLoaded, onError, timeout, myLuaId) 83 | ``` 84 | This will trigger your function, but Lua won't recognize it has loaded the data, for recognizing it, you will need to call in your JS code onload data callback (usually inside Promise.resolve): 85 | ```js 86 | FS.writeFile(luaSaveDir+"/__temp"+luaId, myResolvedPromiseData); 87 | ``` 88 | **Always remember passing "/__temp" with your luaId to your saveDir** 89 | Passing "ERROR" into your myResolvedPromiseData will return as an error in your lua code, this is how you catch errors, you can put additional data for better 90 | error handling 91 | ## 2. From Lua code 92 | If instead you wish to resolve promises inside your own lua code, it is quite simple (although not as flexible when handling from JS): 93 | ```lua 94 | JS.newPromiseRequest(JS.stringFunc( 95 | [[ 96 | var xhttp = new XMLHttpRequest(); 97 | xhttp.onreadystatechange = function() 98 | { 99 | if (this.readyState == 4 && this.status == 200) 100 | _$_(this.responseText); 101 | }; 102 | xhttp.open("GET", "https://example.com", true); 103 | xhttp.send(); 104 | ]] 105 | ``` 106 | With this piece of code, we can directly use `_$_` as a function to load data insided Lua, **_$_** is a syntactic sugar for: 107 | ```lua 108 | [[ FS.writeFile("%s", this.responseText) ]]:format(love.filesystem.getSaveDirectory().."/__temp"..promiseRequestId) 109 | ``` 110 | So, prefer using it, as you can see, it reduces much written code 111 | 112 | 113 | # Integration with Davidobot lovejs 114 | [Davidobot's fork](https://github.com/Davidobot/love.js)' has some differences from the [TannerRogalsky's one](https://github.com/TannerRogalsky/love.js/). 115 | There is a file in which is able to fix the difference about saving onto the file system (which is how the api player works). This file is `globalizeFS.js`. 116 | 117 | It is a nodejs based solution for actually saving the Davidobot's FS into the global scope for the Lua being able to actually execute it. Integrate it with your 118 | build for love.js. After outputting the love.js file, execute `node globalizeFS.js`. It will automatically replace the correct lines with the necessary code. 119 | 120 | ### EXTRA 121 | 1. In lib, it is available too some error handlers, those error occurs only when the retrieveData nevers return, the default value for timeout is 2, but you can change it at your taste 122 | 2. `JS.newRequest` accepts 3 more parameters, the full definition is: `JS.newRequest(funcToCall, onDataLoaded, onError, timeout, optionalId)`, onError is a function that receives the requestID, timeout is a custom parameter for setting if you want to have increased or decreased timeout value, the optionalId is called optional because it will be setup as an incrementing number, but if you want, you can pass a string value for identifying errors 123 | 3. There is another function called `JS.setDefaultErrorFunction`, by setting it up, when your retrieveData returns an error, the function set on the defaultError will be called, there is one already that prints the id of the request if the debug is active 124 | 125 | 126 | # Working Example 127 | - Here is the probably first ever love2d-kongregate integrated game: https://www.kongregate.com/games/MrcSnm/industrial-flying-creature 128 | -------------------------------------------------------------------------------- /consolewrapper.js: -------------------------------------------------------------------------------- 1 | var newConsole = (function(oldConsole) 2 | { 3 | return { 4 | log : function() 5 | { 6 | var data = []; 7 | for (var _i = 0; _i < arguments.length; _i++) { 8 | data[_i] = arguments[_i]; 9 | } 10 | if(data.length == 1) //Start looking for api's (And dont show anything) 11 | { 12 | if(typeof(data[0]) == "string" && data[0].indexOf("callJavascriptFunction") != -1) //Contains function 13 | { 14 | // oldConsole.log(data[0]); 15 | try 16 | { 17 | return eval(data[0].split("callJavascriptFunction ")[1]); 18 | } 19 | catch(e) 20 | { 21 | oldConsole.error("Something went wrong with your callJS: \nCode: " + data[0].split("callJavascriptFunction ")[1] + "\nError: '" + e.message + "'"); 22 | return null; 23 | } 24 | } 25 | else 26 | { 27 | oldConsole.log(data[0]); 28 | return null; 29 | } 30 | } 31 | else 32 | oldConsole.log(data[0], data.splice(1)); 33 | return null; 34 | }, 35 | info : function() 36 | { 37 | var data = []; 38 | for (var _i = 0; _i < arguments.length; _i++) { 39 | data[_i] = arguments[_i]; 40 | } 41 | if(data.length == 1) 42 | oldConsole.info(data[0]); 43 | else 44 | oldConsole.info(data[0], data.splice(1)); 45 | }, 46 | warn : function() 47 | { 48 | var data = []; 49 | for (var _i = 0; _i < arguments.length; _i++) { 50 | data[_i] = arguments[_i]; 51 | } 52 | if(data.length == 1) 53 | oldConsole.warn(data[0]); 54 | else 55 | oldConsole.warn(data[0], data.splice(1)); 56 | }, 57 | error : function() 58 | { 59 | var data = []; 60 | for (var _i = 0; _i < arguments.length; _i++) { 61 | data[_i] = arguments[_i]; 62 | } 63 | if(data.length == 1) 64 | oldConsole.error(data[0]); 65 | else 66 | oldConsole.error(data[0], data.splice(1)); 67 | }, 68 | clear : function() 69 | { 70 | oldConsole.clear() 71 | }, 72 | assert : function() 73 | { 74 | for (var _i = 0; _i < arguments.length; _i++) { 75 | data[_i] = arguments[_i]; 76 | } 77 | oldConsole.assert(data[0], data[1], data.splice(2)); 78 | }, 79 | group : function() 80 | { 81 | for (var _i = 0; _i < arguments.length; _i++) { 82 | data[_i] = arguments[_i]; 83 | } 84 | oldConsole.group(data[0], data.splice(1)); 85 | }, 86 | groupCollapsed : function() 87 | { 88 | for (var _i = 0; _i < arguments.length; _i++) { 89 | data[_i] = arguments[_i]; 90 | } 91 | oldConsole.groupCollapsed(data[0], data.splice(1)); 92 | }, 93 | groupEnd : function() 94 | { 95 | oldConsole.groupEnd() 96 | } 97 | } 98 | }(window.console)); 99 | window.console = newConsole; 100 | -------------------------------------------------------------------------------- /globalizeFS.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const lovejs = path.resolve("love.js"); 5 | 6 | function globalizeFS(sourceCode) 7 | { 8 | const isGlobalizedChecker = "window.FS = null;"; 9 | if(sourceCode.substring(0, isGlobalizedChecker.length) == isGlobalizedChecker) 10 | { 11 | console.log("love.js FS globalization is up to date"); 12 | return sourceCode; 13 | } 14 | 15 | var syscallsIndex = sourceCode.indexOf("var SYSCALLS"); 16 | return "window.FS = null;" + sourceCode.substring(0, syscallsIndex) + 17 | "if(window.FS == null)window.FS = FS;" + sourceCode.substring(syscallsIndex); 18 | } 19 | 20 | 21 | if(fs.existsSync(lovejs)) 22 | { 23 | const source = String(fs.readFileSync(lovejs)); 24 | if(source == null) 25 | throw new Error("Error occurred while reading file " + lovejs); 26 | fs.writeFileSync(lovejs, globalizeFS(source)); 27 | } 28 | else 29 | throw new Error("File love.js not found"); -------------------------------------------------------------------------------- /js.lua: -------------------------------------------------------------------------------- 1 | __requestQueue = {} 2 | _requestCount = 0 3 | _Request = 4 | { 5 | command = "", 6 | currentTime = 0, 7 | timeOut = 2, 8 | id = '0' 9 | } 10 | local os = love.system.getOS() 11 | local __defaultErrorFunction = nil 12 | local isDebugActive = false 13 | 14 | JS = {} 15 | 16 | function JS.callJS(funcToCall) 17 | if(os == "Web") then 18 | print("callJavascriptFunction " .. funcToCall) 19 | end 20 | end 21 | 22 | --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 23 | --If you pass arguments to the func beyond the string, it will perform automatically string.format 24 | --Return statement is possible inside this structure 25 | --This will return a string containing a function to be called by JS.callJS 26 | local _unpack 27 | if(_VERSION == "Lua 5.1" or _VERSION == "LuaJIT") then 28 | _unpack = unpack 29 | else 30 | _unpack = table.unpack 31 | end 32 | function JS.stringFunc(str, ...) 33 | str = "(function(){"..str.."})()" 34 | if(#arg > 0) then 35 | str = str:format(_unpack(arg)) 36 | end 37 | str = str:gsub("[\n\t]", "") 38 | return str 39 | end 40 | 41 | --The call will store in the webDB the return value from the function passed 42 | --it timeouts 43 | local function retrieveJS(funcToCall, id) 44 | --Used for retrieveData function 45 | JS.callJS("FS.writeFile('"..love.filesystem.getSaveDirectory().."/__temp"..id.."', "..funcToCall..");") 46 | end 47 | 48 | --Call JS.newRequest instead 49 | function _Request:new(isPromise, command, onDataLoaded, onError, timeout, id) 50 | local obj = {} 51 | setmetatable(obj, self) 52 | obj.command = command 53 | obj.onError = onError or __defaultErrorFunction 54 | if not isPromise then 55 | retrieveJS(command, id) 56 | else 57 | JS.callJS(command) 58 | end 59 | obj.onDataLoaded = onDataLoaded 60 | obj.timeOut = (timeout == nil) and obj.timeOut or timeout 61 | obj.id = id 62 | 63 | 64 | function obj:getData() 65 | --Try to read from webdb 66 | return love.filesystem.read("__temp"..self.id) 67 | end 68 | 69 | function obj:purgeData() 70 | --Data must be purged for not allowing old data to be retrieved 71 | love.filesystem.remove("__temp"..self.id) 72 | end 73 | 74 | function obj:update(dt) 75 | self.timeOut = self.timeOut - dt 76 | local retData = self:getData() 77 | 78 | if((retData ~= nil and retData ~= "nil") or self.timeOut <= 0) then 79 | if(retData ~= nil and retData:match("ERROR") == nil) then 80 | if isDebugActive then 81 | print("Data has been retrieved "..retData) 82 | end 83 | self.onDataLoaded(retData) 84 | else 85 | self.onError(self.id, retData) 86 | end 87 | self:purgeData() 88 | return false 89 | else 90 | return true 91 | end 92 | end 93 | return obj 94 | end 95 | 96 | --Place this function on love.update and set it to return if it returns false (This API is synchronous) 97 | function JS.retrieveData(dt) 98 | local isRetrieving = #__requestQueue ~= 0 99 | local deadRequests = {} 100 | for i = 1, #__requestQueue do 101 | local isUpdating =__requestQueue[i]:update(dt) 102 | if not isUpdating then 103 | table.insert(deadRequests, i) 104 | end 105 | end 106 | for i = 1, #deadRequests do 107 | if(isDebugActive) then 108 | print("Request died: "..deadRequests[i]) 109 | end 110 | table.remove(__requestQueue, deadRequests[i]) 111 | end 112 | return isRetrieving 113 | end 114 | 115 | --May only be used for functions that don't return a promise 116 | function JS.newRequest(funcToCall, onDataLoaded, onError, timeout, optionalId) 117 | if(os ~= "Web") then 118 | return 119 | end 120 | table.insert(__requestQueue, _Request:new(false, funcToCall, onDataLoaded, onError, timeout or 5, optionalId or _requestCount)) 121 | end 122 | 123 | --This function can be handled manually (in JS code) 124 | --How to: add the function call when your events resolve: FS.writeFile("Put love.filesystem.getSaveDirectory here", "Pass a string here (NUMBER DONT WORK")) 125 | --Or it can be handled by Lua, it auto sets your data if you write the following command: 126 | -- _$_(yourStringOrFunctionHere) 127 | function JS.newPromiseRequest(funcToCall, onDataLoaded, onError, timeout, optionalId) 128 | if(os ~= "Web") then 129 | return 130 | end 131 | optionalId = optionalId or _requestCount 132 | funcToCall = funcToCall:gsub("_$_%(", "FS.writeFile('"..love.filesystem.getSaveDirectory().."/__temp"..optionalId.."', ") 133 | table.insert(__requestQueue, _Request:new(true, funcToCall, onDataLoaded, onError, timeout or 5, optionalId)) 134 | end 135 | 136 | 137 | --It receives the ID from ther request 138 | --Don't try printing the request.command, as it will execute the javascript command 139 | function JS.setDefaultErrorFunction(func) 140 | __defaultErrorFunction = func 141 | end 142 | 143 | JS.setDefaultErrorFunction(function(id, error) 144 | if( isDebugActive ) then 145 | local msg = "Data could not be loaded for id:'"..id.."'" 146 | if(error)then 147 | msg = msg.."\nError: "..error 148 | end 149 | print(msg) 150 | end 151 | end) 152 | 153 | 154 | JS.callJS(JS.stringFunc( 155 | [[ 156 | __getWebDB("%s"); 157 | ]] 158 | , "__LuaJSDB")) 159 | -------------------------------------------------------------------------------- /webdb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marcelo Silva Nascimento Mancini 3 | * @github https://github.com/MrcSnm/Love.js-Api-Player 4 | */ 5 | 6 | //This line is needed for retrieving data from .lua 7 | //Please remember that this is not a flexible API, it should be used with consolewrapper.js + love.js 8 | //This will activate the web database for using in conjunction to .lua + love.js 9 | window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 10 | if(!window.indexedDB) 11 | { 12 | alert("Please, update your browser to newer versions or you won't be able to save your game") 13 | } 14 | 15 | var __webDb = null; 16 | var __webTunnel = "" 17 | var __currRequest = null 18 | async function __getWebDB(dbName) 19 | { 20 | if(__webTunnel == "") 21 | __webTunnel = dbName; 22 | return new Promise(function(resolve, reject) 23 | { 24 | if(__webDb != null) 25 | { 26 | console.log("Already connected with WebDB") 27 | return resolve(); 28 | } 29 | if(__currRequest != null) 30 | { 31 | let success = __currRequest.onsuccess 32 | __currRequest.onsuccess = function() 33 | { 34 | success() 35 | console.log("Request stack completed") 36 | return resolve(); 37 | } 38 | let blocked = __currRequest.onblocked 39 | __currRequest.onblocked = function() 40 | { 41 | blocked() 42 | return reject(); 43 | } 44 | } 45 | else 46 | { 47 | __currRequest = indexedDB.open(dbName); 48 | __currRequest.onerror = function() 49 | { 50 | console.error("Some error ocurred when trying to access web database '" + dbName + "'"); 51 | reject(); 52 | } 53 | //This webdb should not change version, so it will not add any field 54 | __currRequest.onupgradeneeded = function() 55 | { 56 | __webDb = __currRequest.result; 57 | __webDb.createObjectStore("FILE_DATA"); 58 | } 59 | __currRequest.onblocked = function(ev) 60 | { 61 | console.error("Access to the web database were blocked, please refresh the page"); 62 | reject(); 63 | } 64 | __currRequest.onsuccess = function() 65 | { 66 | __webDb = __currRequest.result; 67 | console.log("Connected with WebDB '"+dbName+"'") 68 | } 69 | } 70 | }) 71 | } 72 | 73 | /** 74 | * This function must not be called manually 75 | * @param {any} val 76 | */ 77 | function ___convertToUint8Array(val) 78 | { 79 | val = String(val); 80 | let content = new Uint8Array(val.length); 81 | for(i = 0, len = val.length; i < len; i++) 82 | { 83 | content[i] = (val[i]).charCodeAt(0); 84 | } 85 | return content; 86 | } 87 | 88 | /** 89 | * This function must not be called manually 90 | * @param {any} value 91 | */ 92 | function ___getLoveJSCompatibleObject(value) 93 | { 94 | value = ___convertToUint8Array(value); //Content must be Uint8Array 95 | 96 | return { 97 | timestamp: Date.now(), 98 | mode : 33152, //For some reason it is the mode used for when doing love.filesystem.write 99 | contents: value //Uint8Array 100 | } 101 | } 102 | 103 | async function __storeWebDB(value, objStore, where) 104 | { 105 | await __getWebDB(__webTunnel); 106 | console.log("Storing value '" + value + "'") 107 | let lovejsCompatibleObject = ___getLoveJSCompatibleObject(value) 108 | 109 | let transaction = __webDb.transaction(objStore, "readwrite"); 110 | let store = transaction.objectStore(objStore); 111 | let request = store.put(lovejsCompatibleObject, where); 112 | request.onsuccess = function() 113 | { 114 | console.log("'" + value + "' were succesfully added to "+where); 115 | } 116 | request.onerror = function() 117 | { 118 | console.error("Could not add the value '" + value + "' to "+where +": " + request.error); 119 | } 120 | } 121 | 122 | function __unconvertFromUint8Array(arr) 123 | { 124 | return String.fromCharCode(arr); 125 | } 126 | 127 | /** 128 | * Please use it only for debugging purposes 129 | * @param {String= "FILE_DATA"} objStore 130 | */ 131 | async function __readWebDB(objStore, where) 132 | { 133 | await __getWebDB(__webTunnel); 134 | objStore = (objStore == undefined) ? "FILE_DATA" : objStore; 135 | let transaction = __webDb.transaction(objStore, "readonly"); 136 | let store = transaction.objectStore(objStore); 137 | let request = store.get(where); 138 | request.onsuccess = function() 139 | { 140 | console.log("Value gotten: " + __unconvertFromUint8Array(request.result.contents)); 141 | } 142 | } 143 | 144 | if(typeof FS === 'undefined') 145 | { 146 | FS = {}; 147 | FS.writeFile = function(where, content) 148 | { 149 | __storeWebDB(content, "FILE_DATA", where); 150 | } 151 | } 152 | --------------------------------------------------------------------------------