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.

index ↑

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.

index ↑

Settings

caching( [ en ] )

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.

index ↑

debug( [ en ] )

local patcher = require "plugin.patcher"
patcher.debug( true ) -- Turn on 'verbose' debug output for this plugin.

index ↑

enabled( [ en ] )

local patcher = require "plugin.patcher"
patcher.enabled( false ) -- Disable patching

-- Now, all future calls to require() will return references to original scripts.

index ↑

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()

index ↑

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 )

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.

index ↑

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() 

index ↑

patched( path )

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" )

index ↑

purge( [ path ] )

local patcher = require "plugin.patcher"

-- Purge a single script:
--
patcher.purge( "scripts.myModule" )

-- Purge entire cache:
--
patcher.purge( )

index ↑

remove( path )

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" )

index ↑

Helpers

get( src, dst [, onSuccess [, onFail [, onProgress ]]] )

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 )

index ↑

write( script, path )

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" )

index ↑

mkFolder( path )

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" )

index ↑

Complete Example

You can download a complete example that tests the features of the plugin here:

patcher-example.zip

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

RoamingGamer Copyright © Roaming Gamer, LLC. 2008-2017; All Rights Reserved