Patcher
Patcher is a plugin that allows you to execute downloaded scripts as if they were part of your app/game distribution.
Have you ever wondered, "Is there a drop-dead easy way to download and execute scripts in my released app/game?"
If so, the answer is, "Yes!".
With patcher you can download scripts to effectively supercede scripts bundled with your app. You can also download new code and execute it.
Basic Usage
A. Activate Plugin
First, purchase the plugin on Corona Marketplace to activate it.
B. Update build.settings
Second, ensure your build.settings file has this code in it:
settings =
{
plugins =
{
["plugin.patcher"] = { publisherId = "com.roaminggamer" },
},
}
C. Enable Patcher
Once you have downloaded replacement scripts you can enable it by doing this at the very top of main.lua.
local patcher = require "plugin.patcher"
patcher.export()
Now, you can use the require()
function as you normally would and any 'downloaded scripts' will automatically be used in place of the ones in the system.ResourceDirectory.
(Please see the Docs below for more details.)
Docs Index
Alphabetical Function Index
Function | Description |
---|---|
patcher.caching | This function allows you to enable/disable script caching. |
patcher.debug | This feature allows you to enable/debug verbose debug messaging. |
patcher.dump | This feature dumps a list of all cached scripts to the console (if verbose mode enabled). It also returns a alphabetically sorted table listing the the script names. |
patcher.enabled | This function allows you to enable and disable patching without purging or removing patches. |
patcher.export | This function allows you to replace the global require() function with patcher.require() . |
patcher.get | This is a helper function that allows you download scripts to the correct path and file for superceding existing scripts. |
patcher.getSettings | Returns a table of the current settings ( caching, enabled, verbose) and their values. |
patcher.mkFolder | This helper function can be used to create any folders you need prior to generating or downloading scripts in the system.DocumentsDirectory. |
patcher.patched | Returns true if the named script has been patched. |
patcher.purge | Allows you to purge a single named script, or all scripts if no specific script is specfied. |
patcher.reset | This function restores the global _G.require() reference to the standard function. |
patcher.remove | This function deletes a named patch (script) from the specified system.DocumentsDirectory folder. |
patcher.require | This is the replacement for Corona's standard require() function |
patcher.write | This helper function allows you to write a script to a specific location in the system.DocumentsDirectory. |
Initialization
export()
Replaces the global require()
function with patcher.require()
.
Tip: This should be done ONCE at the very top of main.lua.
local patcher = require "plugin.patcher"
patcher.export()
-- Now, all future calls to require() will use the patcher version of this function.
reset()
Restore the global reference to the standard function _G.require()
.
local patcher = require "plugin.patcher"
patcher.reset()
-- Now, all future calls to require() will use the original version of this function.
Settings
caching( [ en ] )
en
-true
to enable;false
to disable. Caching is enabled by default.
Tip: While caching is disabled, scripts are re-evaluated on each require(). This still updates the cache.
local patcher = require "plugin.patcher"
patcher.caching( false ) -- Disable patching
-- Now, all future calls to require() will reload scripts from file.
debug( [ en ] )
en
-true
to enable verbose debug messaging;false
to disable verbose debug messaging. Verbose mode is diasbled by default.
local patcher = require "plugin.patcher"
patcher.debug( true ) -- Turn on 'verbose' debug output for this plugin.
enabled( [ en ] )
en
-true
to enable;false
to disable. Patching is enabled by default.
local patcher = require "plugin.patcher"
patcher.enabled( false ) -- Disable patching
-- Now, all future calls to require() will return references to original scripts.
getSettings()
Returns a table containing the names and values of the current settings: caching, enabled, verbose.
local patcher = require "plugin.patcher"
local patcherSettings = patcher.getSettings()
Patching
Patching is a fancy way of saying, we are going to generate or download a script into the system.DocumentsFolder and then use it instead of a script we have in the system.ResourceDirectory.
This plugin also allows you to generate/download new scripts that were never part of your original app, thus extending it.
require( path )
path
- Path to script (minus lua extension) just like a normal call to require
local patcher = require "plugin.patcher"
-- Load the patched version of 'myModule' if it is in system.DocumentsDirectory:
-- >> scripts/myModule.lua
--
-- Alternately, if no file is found use the original script.
--
local myModule = patcher.require( "scripts.myModule" )
Tip: While are free to use the long-hand version of this, it is far better and easier to simply export patcher's replacement function and then call require()
as you normally would.
Utilities
dump( )
If verbose debugging mode is enable, this will print a list of patched scripts to the console.
This always returns an alphabetized table of the patched script names too.
local patcher = require "plugin.patcher"
local patchedList = patcher.dump()
patched( path )
path
- Normal path to a script.
Returns true
if the specified script has been patched and stored in the patcher's cache.
local patcher = require "plugin.patcher"
local isPatched = patcher.patched( "scripts.myModule" )
purge( [ path ] )
path
- An optional path to a script to purge from the patcher's cache.
local patcher = require "plugin.patcher"
-- Purge a single script:
--
patcher.purge( "scripts.myModule" )
-- Purge entire cache:
--
patcher.purge( )
remove( path )
path
- Normal path to a script.
Deletes a file if found at the specified path (assumes the file is a .lua script).
Warning!: Do not use this as some kind of general file helper. It should only be used for patch files.
local patcher = require "plugin.patcher"
-- Delete the patch for `myModule.lua`
--
patcher.remove( "scripts.myModule" )
Helpers
get( src, dst [, onSuccess [, onFail [, onProgress ]]] )
src
- Full http: or https: path to script.- dst - Normal path to script as you would specify for
require()
calls. onSuccess
- An optional function that is called when the file is 'successfully' downloaded.onFail
- An optional function that is called when the file fails to download.onProgress
- An optional function that is called when the file is downloading. This can be used to give 'progress' feedback if you need it. Most people will not use this.
Warning!: Do not use this as some kind of general file download helper. It should only be used for patch files.
local patcher = require "plugin.patcher"
-- Download a patch for 'myModule.lua'
--
local testPatch = "https://raw.githubusercontent.com/roaminggamer/RG_FreeStuff/master/myPluginSamples/patcher/myModule.lua"
local function onSuccess( event )
print("Success!")
end
local function onFail( event )
print("Failure!")
for k,v in pairs(event) do
print(k,v)
end
end
patcher.get( testPatch, "scripts.myModule", onSuccess, onFail )
write( script, path )
script
- A lua standard script (usually a module).path
- Normal path to script as you would specify forrequire()
calls.
While the typical usage for patcher is to download scripts, there is no reason why you can generate them locally. This helper gives you an easy way to save those 'generated' scripts.
Tip: Remember to make any folders you need before attempting to write scripts to them.
local patcher = require "plugin.patcher"
local patchedScript =
"local m = {}\n" ..
"local cx,cy = display.contentCenterX, display.contentCenterY\n" ..
"local mRand = math.random\n" ..
"function m.run( group )\n" ..
" local tmp = display.newRect( group, cx + mRand(-200,200), cy + mRand(-20,80), 40, 40 )\n" ..
" tmp:setFillColor(1,mRand(),mRand())\n" ..
"display.newText( group, 'patch v2 - scripts.myModule', cx, cy + 160, 'Lato-Black.ttf', 22 )\n" ..
"end\n" ..
"return m"
patcher.mkFolder( "scripts" )
patcher.write( patchedScript, "scripts.myModule" )
mkFolder( path )
path
- A standard file path as you would specify it for regular file operations.
Warning! This is the only case in patcher where paths are specified using forward slashes if needed.
local patcher = require "plugin.patcher"
-- Make a scripts folder and a sub-folder 'other' in scripts
--
patcher.mkFolder( "scripts" )
patcher.mkFolder( "scripts/other" )
Complete Example
You can download a complete example that tests the features of the plugin here:
build.settings
-- =============================================================
-- https://docs.coronalabs.com/daily/guide/distribution/buildSettings/index.html
-- https://docs.coronalabs.com/daily/guide/tvos/index.html
-- https://docs.coronalabs.com/daily/guide/distribution/win32Build/index.html
-- https://docs.coronalabs.com/daily/guide/distribution/osxBuild/index.html
-- =============================================================
local orientation = 'portrait' -- portrait, landscapeRight, ...
settings = {
-------------------------------------------------------------------------------
-- Orientation Settings
-------------------------------------------------------------------------------
orientation = {
default = orientation,
supported = { orientation },
},
android =
{
usesPermissions =
{
"android.permission.INTERNET",
},
},
iphone =
{
plist =
{
UIStatusBarHidden = false,
CFBundleIconFiles =
{
"Icon-40.png",
"Icon-58.png",
"Icon-76.png",
"Icon-80.png",
"Icon-87.png",
"Icon-120.png",
"Icon-152.png",
"Icon-167.png",
"Icon-180.png",
},
},
},
}
main.lua
-- =============================================================
io.output():setvbuf("no")
display.setStatusBar(display.HiddenStatusBar)
-- =============================================================
-- Code To Create Example Buttons ++
-- =============================================================
local widget = require( "widget" )
widget.setTheme( "widget_theme_ios" )
--widget.setTheme( "widget_theme_android_holo_light" )
--widget.setTheme( "widget_theme_android_holo_dark" )
--
local cx,cy = display.contentCenterX, display.contentCenterY
local uw = display.actualContentWidth - display.contentWidth
local uh = display.actualContentHeight - display.contentHeight
local left = (uw == 0) and 0 or (0 - uw/2)
local right = left + display.actualContentWidth
local top = (uh == 0) and 0 or (0 - uh/2)
local bottom = top + display.actualContentHeight
--
local group
--
local myModule
--
local bw = 200
local bh = 30
local bh2 = 80
local function makeButton( x, y, text, action )
local button = display.newRect( x, y, bw, bh )
button:setFillColor(1,1,1,0.1)
button:setStrokeColor(1,1,1,0.5)
button.strokeWidth = 2
button.label = display.newText( text, x, y, "Lato-Black.ttf", 16 )
button.action = action or function() end
function button.touch( self, event )
if( event.phase == "ended" ) then
self:setStrokeColor(0.5,1,0.5,1)
timer.performWithDelay( 60,
function()
self:setStrokeColor(1,1,1,0.5)
self.action()
end )
end
return true
end
button:addEventListener("touch")
return button
end
--
local function makeToggleButton( x, y, text1, text2, text3, offAction, onAction )
local topLabel = display.newText( text1, x, y, "Lato-Black.ttf", 22 )
-- Question: why is the logic opposite? Bug in widget.*?
local function listener( event )
if( event.target.isOn ) then
offAction()
else
onAction()
end
end
-- Create a default on/off switch (using widget.setTheme)
local button = widget.newSwitch
{
x = x,
y = y,
onRelease = listener,
}
button.x = x
button.y = topLabel.y + topLabel.contentHeight/2 + button.contentHeight/2
local onLabel = display.newText( text2, button.x + button.contentWidth/2 + 10, button.y, "Lato-Black.ttf", 18 )
onLabel.anchorX = 0
onLabel:setTextColor(0,1,0)
local offLabel = display.newText( text3, button.x - button.contentWidth/2 - 10, button.y, "Lato-Black.ttf", 18 )
offLabel.anchorX = 1
offLabel:setTextColor(1,0,0)
return button
end
--
local function easyAlert( title, msg, buttons )
buttons = buttons or { {"OK"} }
local function onComplete( event )
local action = event.action
local index = event.index
if( action == "clicked" ) then
local func = buttons[index][2]
if( func ) then func() end
end
end
local names = {}
for i = 1, #buttons do
names[i] = buttons[i][1]
end
local alert = native.showAlert( title, msg, names, onComplete )
return alert
end
-- =============================================================
-- Load patcher and start disabled, quiet, etc.
-- =============================================================
local patcher = require "plugin.patcher"
patcher.debug(false)
patcher.enabled(false)
patcher.caching(false)
-- =============================================================
-- Patcher Tests
-- =============================================================
local function createTestPatch()
local patchedScript =
"local m = {}\n" ..
"local cx,cy = display.contentCenterX, display.contentCenterY\n" ..
"local mRand = math.random\n" ..
"function m.run( group )\n" ..
" local tmp = display.newRect( group, cx + mRand(-200,200), cy + mRand(-20,80), 120, 120 )\n" ..
" tmp:setFillColor(1,mRand(),mRand())\n" ..
"display.newText( group, 'patch v1 - scripts.myModule', cx, cy + 160, 'Lato-Black.ttf', 22 )\n" ..
"end\n" ..
"return m"
patcher.mkFolder( "scripts" )
patcher.write( patchedScript, "scripts.myModule" )
end
local function createTestPatch2()
local patchedScript =
"local m = {}\n" ..
"local cx,cy = display.contentCenterX, display.contentCenterY\n" ..
"local mRand = math.random\n" ..
"function m.run( group )\n" ..
" local tmp = display.newRect( group, cx + mRand(-200,200), cy + mRand(-20,80), 40, 40 )\n" ..
" tmp:setFillColor(1,mRand(),mRand())\n" ..
"display.newText( group, 'patch v2 - scripts.myModule', cx, cy + 160, 'Lato-Black.ttf', 22 )\n" ..
"end\n" ..
"return m"
patcher.mkFolder( "scripts" )
patcher.write( patchedScript, "scripts.myModule" )
end
local function downloadPatch()
patcher.mkFolder( "scripts" )
local function onSuccess( event )
easyAlert("Success", "Patch downloaded!" )
end
local function onFail( event )
easyAlert("Failure", "Patch not downloaded?\n\nSee console" )
for k,v in pairs(event) do
print(k,v)
end
end
local function onProgress( event )
--for k,v in pairs(event) do
--print(k,v)
--end
end
local testPatch = "https://raw.githubusercontent.com/roaminggamer/RG_FreeStuff/master/myPluginSamples/patcher/myModule.lua"
patcher.get( testPatch, "scripts.myModule", onSuccess, onFail, onProgress )
end
local function destroyPatch()
patcher.remove( "scripts.myModule" )
end
local function loadMyModule()
myModule = require "scripts.myModule"
end
local function testMyModule()
if( not myModule ) then return end
display.remove(group)
group = display.newGroup()
myModule.run( group )
end
local function printPatcherSettings()
local settings = patcher.getSettings()
print("=============================")
for k,v in pairs(settings) do
print(k,v)
end
print("=============================\n")
end
-- =============================================================
-- Create Buttons to Run Tests
-- =============================================================
local by = top + 20
local button = makeToggleButton( cx, by, "require() using", "patcher.require()", "_G.require()",
function() patcher.export() end,
function() patcher.reset() end )
by = by + bh2
local button = makeToggleButton( cx, by, "patcher.enabled()", "true", "false",
function() patcher.enabled(true) end,
function() patcher.enabled(false) end )
by = by + bh2
local button = makeToggleButton( cx, by, "patcher.debug()", "true", "false",
function() patcher.debug(true) end,
function() patcher.debug(false) end )
by = by + bh2
local button = makeToggleButton( cx, by, "patcher.caching()", "true", "false",
function() patcher.caching(true) end,
function() patcher.caching(false) end )
by = by + bh2
makeButton( cx, by, "Print Patcher Settings", printPatcherSettings )
by = bottom - 240
local bx = cx - 200
makeButton( bx, by, "Create Patch File v1", createTestPatch ); by = by + bh + 10
makeButton( bx, by, "Create Patch File v2", createTestPatch2 ); by = by + bh + 10
makeButton( bx, by, "Download Patch File v3", downloadPatch ); by = by + bh * 3
makeButton( bx, by, "Destroy Patch File", destroyPatch )
by = bottom - 240
local bx = cx + 200
makeButton( bx, by, "Dump Cache", function() patcher.dump() end); by = by + bh + 10
makeButton( bx, by, "Purge Cache", function() patcher.purge() end ); by = by + bh * 3
makeButton( bx, by, "Load Module", loadMyModule ); by = by + bh + 10
makeButton( bx, by, "Test Module", testMyModule ); by = by + bh * 3
scripts/myModule.lua
local m = {}
local cx,cy = display.contentCenterX, display.contentCenterY
local mRand = math.random
function m.run( group )
local tmp = display.newCircle( group, cx + mRand(-200,200), cy + mRand(-20,80), 60 )
tmp:setFillColor(1,mRand(),mRand())
display.newText( group, 'original - scripts.myModule', cx, cy + 160, 'Lato-Black.ttf', 22 )
end
return m
Copyright © Roaming Gamer, LLC. 2008-2017; All Rights Reserved