Creating Apps: Difference between revisions

From Off Grid Wiki
Jump to navigation Jump to search
m (Harry moved page Creating Apps to Modding/Creating Apps without leaving a redirect)
 
(31 intermediate revisions by 2 users not shown)
Line 1: Line 1:
== Introduction ==
The main tool the player has in Off Grid (apart from clever mind and smooth stealth skills, of course) is the apps running on the player's phone, allowing both viewing and interacting with all the devices and data in the levels.
The app system is built to allow creating all kinds of new tools, both very generic and powerful ones, but also small quality-of-life tools that just do some small very specific task, but make it more convenient for the player.
It can also handle different types of targets, display information in the UI, and do pretty much anything our Lua API allows.
On the game side, we handle the controls, app inventory and other boring common tasks automatically, and also keep track of the app use costs, and if the player is able to use an app at the moment or not, so what's left to the modder is just the specific way how ''your'' app should work.
This page should explain what goes into an app's Lua script, but for more ideas and examples it's worth looking at the existing apps that are included in the game. You'll find them in ''StreamingAssets/Common/Apps'' folder.
== Example Lua script ==
== Example Lua script ==


<syntaxhighlight lang="lua" lines>
<source lang="lua">app = {
app = {
    name = "Example App",
name = "DataDelete",
    description = "How to make your first app for Off Grid.",
description = "Delete a data point.",
    cost = 2,
cost = 2.0,
    appType = AppTypes.Active,
targets = {"data"},
    useSpectrum = true,
showInRadial = "target",
    range = 10,
showInAppWheel = "default",
    targets = {TargetType.Data},
state = "off",
    showInRadial = AppMenuState.target,
icon = "Common/Apps/Icons/DataDelete.png",
    showInAppDock = AppMenuState.byDefault,
iconColor = { 0, 0.4, 0.9843, 1 },
    icon = "ExampleApp.png",
    iconColor = Color.Blue,
    statusIcons = { "option1.png", "option2.png"},
 
    OnTrigger = function(target)
        -- What to do when player triggers this (active) app
    end,
 
 
    OnStateChange = function(state)
        -- This runs when something on game side asks the app to change it's state
        app.state = state
        SetState(app.state)
    end,


OnClickAppWheel = function()
UI.ToggleDataDelete()
end,


OnClickRadial = function()
    Update = function(time)
UI.ToggleDataDelete()
        -- Update will run continuously.
end,
    end,


}
}</source>
</syntaxhighlight>


== App configuration ==
== App configuration ==
The first section of the app script is used to define some default settings and basic information about what the app is and how it works.


{| class="wikitable"
{| class="wikitable"
!colspan="3" | App Table
! Name
|-
! Required/Optional
! Name !! Required/Optional !! Description
! Description
|-
|-
| name || required || Name of the app
| name
|align="center"| required
| Name of the app
|-
|-
| description || required || Short description of what the app does
| description
|align="center"| required
| Short description of what the app does
|-
|-
| cost || optional || How many seconds the tracking progress increases when using this app
| cost
|align="center"| optional
| How many NetPoints using the app costs (or reserves, if Passive app)
|-
|-
| targets || optional || List of what [[#Target types|target types]] the app can have
| appType
|align="center"| optional
| Is this an Active or a Passive app
|-
|-
| showInRadial || optional || How the app should be displayed in Radial menu. See [[#Menu_Options|menu options]].
| useSpectrum
| align="center"|optional
| Should this app automatically turn on the Spectrum when used.
|-
|-
| showInAppWheel || optional || How the app should be displayed in the AppWheel. See [[#Menu_Options|menu options]].
| range
| align="center"|optional
| How far should the Spectrum view & targeting work when using this app
|-
|-
| state || required || Current [[#States|state of the app]].
| targets
|align="center"| optional
| List of what [[#Target_types|target types]] the app can have
|-
|-
| icon || required || Path to icon image
| showInRadial
|align="center"| optional
| How the app should be displayed in Radial menu. See [[#Menu_options|menu options]].
|-
|-
| iconColor || optional || background color for the app icon
| showInAppDock
|align="center"| optional
| How the app should be displayed in the AppDock. See [[#Menu_options|menu options]].
|-
|-
| OnClickAppWheel || optional || The function to run when the app is launched from the AppWheel
| state
|align="center"| required
| Current [[#App_states|state]] of the app.
|-
|-
| OnClickRadial || optional || The function to run when the app is launched from the radial menu
| icon
|align="center"| required
| Path to icon image
|-
| iconColor
|align="center"| optional
| background color for the app icon
|-
| statusIcons
|align="center"| optional
| List of icon images to use for [[#Status_window|status window]]
|}
|}
=== name ===
This is both the human-readable name of the app that will be displayed to the player in the UI and in menus, and also what's used to refer to the app in all your Lua scripts, inventories, and in game saves.
=== description ===
Longer description of what the app is and what it does.
=== cost ===
The cost (NetPoints) of having the app running. This is only needed if your app uses the [[#Update()|Update()]] function, and you actually want it to cost something. Turning on an app that has a cost will require at least that amount of available NetPoints, and then reserves them while the app is enabled.
=== useSpectrum ===
When set to true, the Spectrum (data view) will be automatically toggled on as needed when suing this app.
=== range ===
Thw working range for this app, controls how far away targets can be picked and the max range for the Spectrum. The value is in meters.
=== appType ===
Defines if the app is an active one (something the player triggers to do an action) or a passive one (something that runs in the background, and is toggled on & off instead of using it for an immediate action).
This also relates to app cost, active apps will consume the app's cost in NetPoints when sued, while passive apps will instead reserve the cost in NetPoints for the time the app is running, reducing how many points the player has available for use.
=== targets ===
Targets should include either an individual [[#Target_types|target type]], or a list of target types. If you don't define anything, the game will use ''TargetTypes.None'' by default, allowing the app to work when no target is selected. For sake of clarity, it's recommended to define that in the app script anyway.
<source lang="lua">targets = TargetType.Data,</source>
<source lang="lua">targets = {TargetType.Hackable, TargetType.Character},</source>
Note that TargetType.None still only lets you trigger actions from RadialUI. The AppWheel is intended for more fast actions without interrupting gameplay, and your untargeted action should be triggered by turning the app on. This make sure there's no conflict between physical interactions player does and whatever app happens to be selected at the moment in the AppWheel.
=== showInRadial ===
Rules for if, and how, the app should be listed in the radial UI. See [[#Menu_options|Menu options]] for the details.
=== showInAppDock ===
Rules for if, and how, the app should be listed in the AppDock. See [[#Menu_options|Menu options]] for the details.
=== state ===
The default starting [[#App_states|state]] of the app. This also used to store the apps ''current'' state when the game is running. Most of the time setting this to ''AppState.off'' would work fine, meaning the app is available to the player an listed in menus as defined by the showInRadial and showInAppWheel settings, but isn't doing anything at the moment.
If you want the player to start your mission without this app, and then give it to the player later on (maybe from a device player hacks, or sent by another character during a conversation or something) you can set the state in the app script to ''AppState.unavailable'', and then later on call <code>Apps.RequestAppState(appName, AppState.off)</code> from the Apps API to make thew app available to the player.
Here's an example onClick() function from one of our devices. This would make the app called &quot;MultiTool&quot; available to player (with a bit of delay to make things happen ''after'' player has closed the device UI and the game continues running again), and also displaying a notification to make it look like a new app was downloaded on the player character's phone:
<source lang="lua">onClick = function()
    Scheduler.CallInSecs(function()
        Apps.RequestAppState("MultiTool", AppState.off)
        UI.ShowNotification(NotificationType.download, "System", "Installed new application: <b>MultiTool</b>", 10)
    end, 1)
end,</source>
=== icon ===
Name (and path) to the icon file used for the app in the game. The icon should be a square PNG image, with transparent background and a bit of empty space on the sides so it'll fit nicely to the AppWheel &amp; Radial UI. (The borders and backgrounds seen in-game are part of the UI, so the icon really is just the &quot;logo&quot; of the app).
=== iconColor ===
The background color for the app icon (used in AppWheel, Radial UI and the Apps menu). The color you set here will be what's sued when the app is switched on, while the off/disabled etc colors are calculated automatically. Any alpha value in color is ignored.
You can either defince RGB values, or use one of our preset colors:
<source lang="lua">iconColor = Color.Blue,</source>
<source lang="lua">iconColor = {0.2, 0.8, 0.6, 1.0},</source>
=== statusIcons ===
If you want your app to display a [[#Status_window|status window]], you can use this to define one or more icons to be used with it. Same rules as with the app icon so PNG, transparent background, and some space left on sides.
You can add colors to your icon images if you want to, but for best fit with the game and to be able to [[#SetStatusIconColor()|set the icon color through code]] (rather than having to load a separate image file for different colors) making the icon white works the best.
== Functions ==
There are a few functions that the game tries to call in different situations, so depending on the app you are making you might want to include some of these in your script.
=== OnTrigger(target) ===
OnTrigger function is what gets executed when the player points at a target and triggers an active app.
=== OnStateChange() ===
OnStateChange function handles the game requesting the app to change it's state, to turn the app on, or off, and so on, either because the player pressed a button to do so, or some other Lua script or game system asked for it. Ultimately, the app itself gets to decide how to handle the request, although usually you would want the app to comply. When the game calls this function, it will pass the requested [[#App_states|app state]] to the function.
The minimum you should do is to set the app's state to the requested one, and then trigger the built-in API function to tell the game to change state there as well:
<source lang="lua">OnStateChange = function(state)
    app.state = state
    SetState(app.state)
end,</source>
If the app script doesn't include OnStateChange() function, the game assumes this is all you want to do and as a fallback solution handles it for you automatically.
You can also add additional things the app should do when changing the state, for example to show or hide a [[#Status_window|status window]] as the app is toggled on and off:
<source lang="lua">OnStateChange = function(state)
    if state == AppState.on then
        CreateStatusWindow()
        SetStatusIcon(1)
        SetStatusIconColor(Color.LightGrey)
        DisplayStatusWindow(true)
    elseif state == AppState.off then
        RemoveStatusWindow()
    end
    app.state = state
    SetState(app.state)
end,</source>
=== Update() ===
If the app script has Update() function, it will be called regularly by the game. This allows your apps to do things continuously in the background. The update rate is once every 0.2 seconds, and the game will pass current [[#time|time]] as the parameter.
The Update function is called regardless of the app's current state, so it's up to you to choose if the app should only do things when it's switched on (which would be the usual case) or if you want the app to continue running in the background.
If the app table includes a cost value (in contrary to the cost defined in actions), this will get applied every tick the update runs.
A good example of app using the Update function would be the light monitor app, which checks the light level continuously and then updates that to a [[#Status_window|status window]]:
<source lang="lua">Update = function()
    if state == AppState.on then
        lightLevel = Player.GetLightLevel()
        if lightLevel < 0.06 then
            SetStatusIconColor(Color.Green)
        elseif lightLevel < 0.15 then
            SetStatusIconColor(Color.Blue)
        else
            SetStatusIconColor(Color.LightGrey)
        end
        statusText = string.format("%.1f %%", lightLevel * 100)
        UpdateStatusWindow(statusText)
    end
end,</source>
== Data types ==


=== Target types ===
=== Target types ===


<syntaxhighlight lang="lua">
These are the types of targets available from the game's targeting system. Using ''TargetType.None'' allows the app to work without requiring a target at all. The values are used in the game internally for bitmasking multiple target types at the same time, and in general won't be needed for anything in Lua side, you can just use the more convenient text version in your script.
targets = {"data", "devices"},
</syntaxhighlight>


{| class="wikitable"
{| class="wikitable"
!| Type
!align="center"| Value
!| Description
|-
| TargetType.None
|align="center"| 0
| No target. Use for apps which don't require any target at all.
|-
|-
! Name !! Description
| TargetType.Data
|align="center"| 1
| Any data points currently visible to the player
|-
|-
| none || The app doesn't require any kind of target
| TargetType.Interaction
|align="center"| 2
| Physical interactions in the levels
|-
|-
| data || Any data points currently visible to the player
| TargetType.Hackable
|align="center"| 4
| Hackable devices in player's current networks
|-
|-
| devices || Hackable devices in player's current networks
| TargetType.Character
|align="center"| 8
| People
|}
 
=== Menu options ===
 
{| class="wikitable"
!| Name
!| Description
|-
|-
| characters || People
| AppMenuState.always
| App is always displayed in this menu.
|-
|-
| interactions || Physical interactions in the levels
| AppMenuState.never
| App will never be shown in this menu
|-
| AppMenuState.byDefault
| App will be set as favourite for this menu by default (but can be removed by player)
|-
| AppMenuState.target
| App will always display in Radial menu if current target matches one of the target types for the app.
|}
|}


=== Menu options ===
=== App states ===


{| class="wikitable"
{| class="wikitable"
!| Name
!| Description
|-
|-
! Name !! Description
| AppState.unavailable
| The player doesn't have this app yet
|-
|-
| always || App is  always displayed in this menu.
| AppState.disabled
| App can't be used at the moment, for example no network connection
|-
|-
| never || App will never be shown in this menu
| AppState.off
| App is not doing anything
|-
|-
| default || App will be set as favourite for this menu by default
| AppState.on
| App is switched on and running in the background
|-
|-
| target || App will always display in Radial menu if current target matches the App's targets value.
| AppState.alert
| App displays alert to notify the player about something
|}
|}


=== States ===
=== time ===
 
The game will pass a ''time'' value to the [[#Update()|Update function]]. This is just counting seconds from when the game was started. (Equal to Unity's [https://docs.unity3d.com/ScriptReference/Time-time.html Time.time])
 
=== target ===
 
When the game runs the UpdateActions function, or triggers an action, it will pass ''target'' table to the app script. This contains some information about the current target:


{| class="wikitable"
{| class="wikitable"
!| Data
!| Description
|-
|-
! Name !! Description
| target.name
| the internalName of the current target object
|-
|-
| on || App is currently switched on
| target.type
| [[#Target_types|TargetType]] of the current target
|-
|-
| off || App is switched off
| target.missionObject
| Set if the target is a MissionObject.(Check the missionObject table for more details)
|-
|-
| disabled || App is not available to use at the moment
| target.data
| Set for data targets. Contain s a table with more information (see data table for details)
|-
|-
| alert || Blinking app icon to notify the player about something
| target.interactionType
| Set on interaction targets, this will contain the interactionType
|-
| target.macAddress
| Set on device targets. Contains the device's MAC address as a string.
|}
|}
If the app's targets list includes TargetType.None, and there is no target selected when UpdateAction() or some action is triggered, the target.name will be an empty string, and target.type is set to TargetType.None.
== Built-in functions ==
=== Status window ===
Adding a status window for your app lets you display a small message window for the player near the top left corner of the screen. The windows can have an icon, and a small piece of text. While there's no hard limit on text length (for now, at least), it's best to keep things really short to avoid conflicts with how many other apps the player might be running at the same time, and to allow all of them enough screen space.
The LightMontior app is a good example of using status windows:
<source lang="lua">OnStateChange = function(state)
        if state == AppState.on then
            --UI.ToggleLightMonitor(true)
            CreateStatusWindow()
            SetStatusIcon(1)
            SetStatusIconColor(Color.LightGrey)
            DisplayStatusWindow(true)
        elseif state == AppState.off then
            --UI.ToggleLightMonitor(false)
            RemoveStatusWindow()
        end
        app.state = state
        SetState(app.state)
    end,
    Update = function()
        -- Get player light value
        lightLevel = Player.GetLightLevel()
        -- change icon color:
        if lightLevel < 0.06 then
            SetStatusIconColor(Color.Green)
        elseif lightLevel < 0.15 then
            SetStatusIconColor(Color.Blue)
        else
            SetStatusIconColor(Color.LightGrey)
        end
        -- update status text
        statusText = string.format("%.1f %%", lightLevel * 100)
        UpdateStatusWindow(statusText)
    end,</source>
So, what happens here is we use OnStateChange() to check if the app has been turned on, and if that's the case then we ask the game to create a status window for the app. It's going to be empty, and invisible, to start with. Next we assign an icon for it (first one form the [[#statusIcons|status icons]] list in the app definition) and set that icon's color to light grey (which matches nicely with rest of the UI content). When all that is done,. we tell the game to actually show the window.
The, in the Update() function, we check the light level, and based on it we change the status icon's color, and then also parse the light level into a nice format to display to user.
Finally, in OnStateChange() function, we make sure to remove the status window when the app is switched off.
We ''could'' just choose to show and hide the window as needed, and in some cases you might want to do that in Update() or something to only display the window part of the time. It's still best to actually remove the window completely when the app is not running.
All the functions to handle status windows are explained better below:
==== CreateStatusWindow() ====
This is just a simple instruction for the game that you want your app to have a status window. No further options, the game will create the window for you, with a default Off Grid logo as icon, and no text. The window will be hidden by default.
==== DisplayStatusWindow() ====
Use <code>DisplayStatusWindow(true)</code> and <code>DisplayStatusWindow(false)</code> to show and hide the window.
==== RemoveStatusWindow() ====
When a window is not needed any longer, for example when the app is switched off, use this to remove it from the UI.
==== SetStatusIcon() ====
SetStatusIcon() sets the status window's icon to one of the icons defined in the statusIcons list in your app definition. Keep in mind that unlike most programming languages, Lua lists start from 1, so to se the first icon you'd run <code>SetStatusIcon(1)</code>.
==== SetStatusIconColor() ====
If you want to change the icon color, SetStatusIconColor() lets you do that. As with everything else in Off Grid scripting, you can either define the color as RGBA values, or use one of our pre-defined colors.
For the best results you should make your original icon image pure white, the color set here will multiply any color values from the icon image with the new color, so anything else than white (or at least grayscale) icon mgiht result in unexpected colors...
using pre-defined color:
<source lang="lua">SetStatusIconColor(Color.Blue)</source>
using a table to define red, green, blue and alpha values:
<source lang="lua">SetStatusIconColor({0.2, 0.4, 0.9, 1.0})</source>
=== App Window ===
Apps can also open a separate UI window of their own, with a background picture, optional progress bar, and few line's worth of space for displaying some text to the player.
Be aware that there can only be one AppWindow at a time, any attempts to open a new one while some other app is already displaying a window will just be silently ignored.
A good example of this, using a Lua coroutine to add some delays to the output, would be in the FlutterSearch app:
<source lang="lua">
app = {
name = "FlutterSearch",
description = "Search the local networks for files with matching Flutter ads cookie",
targets = { TargetType.Character },
range = 10.0,
cost = 5,
useSpectrum = true,
appType = AppTypes.Active,
showInRadial = AppMenuState.target,
showInAppDock = AppMenuState.byDefault,
state = AppState.off,
icon = "FlutterSearch.png",
iconColor = Color.Cyan,
    OnTrigger =  function(target)
app.targetCharacter = target
StartLuaCoroutine(app.DownloadRoutine)
    end,
}
function app:DownloadRoutine()
    -- Get all the target character's data from Spectrum:
    local dataPoints = DataPoints.GetObjectDataPoints(app.targetCharacter.name)
    -- filter for only points with metadata value
    local pointsToSave = {}   
    if #dataPoints > 0 then
        for _, d in ipairs(dataPoints) do
            local metavalue = Player.GetDataPointMetaValue(d)
            if metavalue >= 1 then
                table.insert(pointsToSave, d)
            end
        end
    end
    Apps.Console_OutputLine(app.name, "Searching for files with Flutter ads cookie: " .. app.targetCharacter.name )
    if #pointsToSave > 0 then -- Found some valuable data, let's start "downloading" the files
        -- Open the AppWindow
        CreateAppWindow("FlutterSearch", "FlutterSearch_AppWindowBkg.png")
        ShowAppWindowProgress(true)
        ShowAppWindow()
        local progressPerPoint = 0.5/#pointsToSave
        local progress = 0
        local windowText = "Found Flutter ad profile '" .. app.targetCharacter.name .. "'"
        -- First some filler log messages, and a popup while "working" for next 5 seconds
        Apps.Console_OutputLine(app.name, windowText)
        SetAppWindowContent(windowText)
        for i = 0, 50, 1 do
            progress = progress + 0.1
            SetAppWindowProgress(progress)
            coroutine.yield(0.1)
        end
       
        windowText = windowText .. "\n" .. #pointsToSave .. " files found.</size>"
        SetAppWindowContent(windowText)
        coroutine.yield(1)
        -- Then "download" files one by one
        windowText = windowText .. "\nDownloading files..."
        SetAppWindowContent(windowText)
        for _, d in ipairs(pointsToSave) do
            Apps.Console_OutputLine(app.name, "cp -n //" .. app.targetCharacter.name .. "/" .. d.info.dataPointName .. " ~/Dowloads/" .. d.info.dataPointName)
            Spectrum.SaveDataPoint(d)
            progress = progress + progressPerPoint
            SetAppWindowProgress(progress)
            coroutine.yield(0.1)
        end
        windowText = windowText .. "\nCompleted, closing connection."
        SetAppWindowContent(windowText)
        coroutine.yield(1)
        RemoveAppWindow()
    else  -- no data of any value, say "sorry" and refund the points:
        local noDataText = "Could not find any data for" .. app.targetCharacter.name
        Apps.Console_OutputLine(app.name, noDataText )
        UI.ShowPopUp(PopupType.generic, "FlutterSearch", noDataText, 3)
        Player.RecoverNetPoints(app.cost)
    end
</source>
==== CreateAppWindow("window title", "backgroundImage.png") ====
Creates the app window, and sets the window's tiutle and background. The window will not be visible at this point.
==== ShowAppWindowProgress(true|false) ====
Enable/disable a progress bar at the boittom of the window.
==== ShowAppWindow() ====
Calling this will make the app window visible to the player. Typically you'd do this after setting the window content.
==== SetAppWindowContent("content text") ====
Sets the content text of the app window.
==== SetAppWindowProgress(0-1) ====
Sets the progress bar at the botom of the window (if enabled). Use values from 0 to 1.
==== RemoveAppWindow() ====
Hides the app window and removes it.
=== App state ===
As explained in the [[#OnSTateChange()|OnStateChange()]], to make sure it's always the Lua app that determines what the app does (perhaps excluding some extreme cases like player phone shutting down due to running out of battery etc), nothing on the game side can directly tell the app to change it's state. Instead, the game will ''ask'' the Lua app to do so, and the app can then do it, or choose not to. Once you have determined what the app does, you should then communicate that back to the game, using SetState() function.
==== SetState() ====
Use <code>SetState(app.state)</code> to set the App's state on the game side to match the state your Lua script is in at the moment. This will be reflected on the app's icon color in AppWheel/RadialUI, and also in player being able to (or not) to use the app.
== Apps in your mission and other scripts ==
Some of the app functionality can be triggered from your other Lua scripts. Most commonly, you'd want use this to make apps available to the player during your mission rather than from the start, but there are of course more creative ways to make use of the API. It's worth remembering, though, that in the end it's the App's Lua script that determines what the app does, anything else can only ''request'' it to do something.
=== Apps API ===
The full Apps Lua API is documented [[Apps_Lua_API|here]].
[[Category:Modding]]

Latest revision as of 15:16, 15 November 2023

Introduction

The main tool the player has in Off Grid (apart from clever mind and smooth stealth skills, of course) is the apps running on the player's phone, allowing both viewing and interacting with all the devices and data in the levels.

The app system is built to allow creating all kinds of new tools, both very generic and powerful ones, but also small quality-of-life tools that just do some small very specific task, but make it more convenient for the player.

It can also handle different types of targets, display information in the UI, and do pretty much anything our Lua API allows.

On the game side, we handle the controls, app inventory and other boring common tasks automatically, and also keep track of the app use costs, and if the player is able to use an app at the moment or not, so what's left to the modder is just the specific way how your app should work.

This page should explain what goes into an app's Lua script, but for more ideas and examples it's worth looking at the existing apps that are included in the game. You'll find them in StreamingAssets/Common/Apps folder.

Example Lua script

app = {
    name = "Example App",
    description = "How to make your first app for Off Grid.",
    cost = 2,
    appType = AppTypes.Active,
    useSpectrum = true,
    range = 10,
    targets = {TargetType.Data},
    showInRadial = AppMenuState.target,
    showInAppDock = AppMenuState.byDefault,
    icon = "ExampleApp.png",
    iconColor = Color.Blue,
    statusIcons = { "option1.png", "option2.png"},

    OnTrigger = function(target)
         -- What to do when player triggers this (active) app
    end,


    OnStateChange = function(state)
        -- This runs when something on game side asks the app to change it's state
        app.state = state
        SetState(app.state)
    end,


    Update = function(time)
        -- Update will run continuously.
    end,

}

App configuration

The first section of the app script is used to define some default settings and basic information about what the app is and how it works.

Name Required/Optional Description
name required Name of the app
description required Short description of what the app does
cost optional How many NetPoints using the app costs (or reserves, if Passive app)
appType optional Is this an Active or a Passive app
useSpectrum optional Should this app automatically turn on the Spectrum when used.
range optional How far should the Spectrum view & targeting work when using this app
targets optional List of what target types the app can have
showInRadial optional How the app should be displayed in Radial menu. See menu options.
showInAppDock optional How the app should be displayed in the AppDock. See menu options.
state required Current state of the app.
icon required Path to icon image
iconColor optional background color for the app icon
statusIcons optional List of icon images to use for status window

name

This is both the human-readable name of the app that will be displayed to the player in the UI and in menus, and also what's used to refer to the app in all your Lua scripts, inventories, and in game saves.

description

Longer description of what the app is and what it does.

cost

The cost (NetPoints) of having the app running. This is only needed if your app uses the Update() function, and you actually want it to cost something. Turning on an app that has a cost will require at least that amount of available NetPoints, and then reserves them while the app is enabled.

useSpectrum

When set to true, the Spectrum (data view) will be automatically toggled on as needed when suing this app.

range

Thw working range for this app, controls how far away targets can be picked and the max range for the Spectrum. The value is in meters.

appType

Defines if the app is an active one (something the player triggers to do an action) or a passive one (something that runs in the background, and is toggled on & off instead of using it for an immediate action). This also relates to app cost, active apps will consume the app's cost in NetPoints when sued, while passive apps will instead reserve the cost in NetPoints for the time the app is running, reducing how many points the player has available for use.

targets

Targets should include either an individual target type, or a list of target types. If you don't define anything, the game will use TargetTypes.None by default, allowing the app to work when no target is selected. For sake of clarity, it's recommended to define that in the app script anyway.

targets = TargetType.Data,
targets = {TargetType.Hackable, TargetType.Character},

Note that TargetType.None still only lets you trigger actions from RadialUI. The AppWheel is intended for more fast actions without interrupting gameplay, and your untargeted action should be triggered by turning the app on. This make sure there's no conflict between physical interactions player does and whatever app happens to be selected at the moment in the AppWheel.

showInRadial

Rules for if, and how, the app should be listed in the radial UI. See Menu options for the details.

showInAppDock

Rules for if, and how, the app should be listed in the AppDock. See Menu options for the details.

state

The default starting state of the app. This also used to store the apps current state when the game is running. Most of the time setting this to AppState.off would work fine, meaning the app is available to the player an listed in menus as defined by the showInRadial and showInAppWheel settings, but isn't doing anything at the moment.

If you want the player to start your mission without this app, and then give it to the player later on (maybe from a device player hacks, or sent by another character during a conversation or something) you can set the state in the app script to AppState.unavailable, and then later on call Apps.RequestAppState(appName, AppState.off) from the Apps API to make thew app available to the player.

Here's an example onClick() function from one of our devices. This would make the app called "MultiTool" available to player (with a bit of delay to make things happen after player has closed the device UI and the game continues running again), and also displaying a notification to make it look like a new app was downloaded on the player character's phone:

onClick = function()
    Scheduler.CallInSecs(function()
        Apps.RequestAppState("MultiTool", AppState.off)
        UI.ShowNotification(NotificationType.download, "System", "Installed new application: <b>MultiTool</b>", 10)
    end, 1)
end,

icon

Name (and path) to the icon file used for the app in the game. The icon should be a square PNG image, with transparent background and a bit of empty space on the sides so it'll fit nicely to the AppWheel & Radial UI. (The borders and backgrounds seen in-game are part of the UI, so the icon really is just the "logo" of the app).

iconColor

The background color for the app icon (used in AppWheel, Radial UI and the Apps menu). The color you set here will be what's sued when the app is switched on, while the off/disabled etc colors are calculated automatically. Any alpha value in color is ignored.

You can either defince RGB values, or use one of our preset colors:

iconColor = Color.Blue,
iconColor = {0.2, 0.8, 0.6, 1.0},

statusIcons

If you want your app to display a status window, you can use this to define one or more icons to be used with it. Same rules as with the app icon so PNG, transparent background, and some space left on sides.

You can add colors to your icon images if you want to, but for best fit with the game and to be able to set the icon color through code (rather than having to load a separate image file for different colors) making the icon white works the best.

Functions

There are a few functions that the game tries to call in different situations, so depending on the app you are making you might want to include some of these in your script.


OnTrigger(target)

OnTrigger function is what gets executed when the player points at a target and triggers an active app.

OnStateChange()

OnStateChange function handles the game requesting the app to change it's state, to turn the app on, or off, and so on, either because the player pressed a button to do so, or some other Lua script or game system asked for it. Ultimately, the app itself gets to decide how to handle the request, although usually you would want the app to comply. When the game calls this function, it will pass the requested app state to the function.

The minimum you should do is to set the app's state to the requested one, and then trigger the built-in API function to tell the game to change state there as well:

OnStateChange = function(state)
    app.state = state
    SetState(app.state)
end,

If the app script doesn't include OnStateChange() function, the game assumes this is all you want to do and as a fallback solution handles it for you automatically.

You can also add additional things the app should do when changing the state, for example to show or hide a status window as the app is toggled on and off:

OnStateChange = function(state)
    if state == AppState.on then
        CreateStatusWindow()
        SetStatusIcon(1)
        SetStatusIconColor(Color.LightGrey)
        DisplayStatusWindow(true)
    elseif state == AppState.off then
        RemoveStatusWindow()
    end
    app.state = state
    SetState(app.state)
end,

Update()

If the app script has Update() function, it will be called regularly by the game. This allows your apps to do things continuously in the background. The update rate is once every 0.2 seconds, and the game will pass current time as the parameter.

The Update function is called regardless of the app's current state, so it's up to you to choose if the app should only do things when it's switched on (which would be the usual case) or if you want the app to continue running in the background.

If the app table includes a cost value (in contrary to the cost defined in actions), this will get applied every tick the update runs.

A good example of app using the Update function would be the light monitor app, which checks the light level continuously and then updates that to a status window:

Update = function()
    if state == AppState.on then
        lightLevel = Player.GetLightLevel()
        if lightLevel < 0.06 then
            SetStatusIconColor(Color.Green)
        elseif lightLevel < 0.15 then
            SetStatusIconColor(Color.Blue)
        else
            SetStatusIconColor(Color.LightGrey)
        end
        statusText = string.format("%.1f %%", lightLevel * 100)
        UpdateStatusWindow(statusText)
    end
end,

Data types

Target types

These are the types of targets available from the game's targeting system. Using TargetType.None allows the app to work without requiring a target at all. The values are used in the game internally for bitmasking multiple target types at the same time, and in general won't be needed for anything in Lua side, you can just use the more convenient text version in your script.

Type Value Description
TargetType.None 0 No target. Use for apps which don't require any target at all.
TargetType.Data 1 Any data points currently visible to the player
TargetType.Interaction 2 Physical interactions in the levels
TargetType.Hackable 4 Hackable devices in player's current networks
TargetType.Character 8 People

Menu options

Name Description
AppMenuState.always App is always displayed in this menu.
AppMenuState.never App will never be shown in this menu
AppMenuState.byDefault App will be set as favourite for this menu by default (but can be removed by player)
AppMenuState.target App will always display in Radial menu if current target matches one of the target types for the app.

App states

Name Description
AppState.unavailable The player doesn't have this app yet
AppState.disabled App can't be used at the moment, for example no network connection
AppState.off App is not doing anything
AppState.on App is switched on and running in the background
AppState.alert App displays alert to notify the player about something

time

The game will pass a time value to the Update function. This is just counting seconds from when the game was started. (Equal to Unity's Time.time)

target

When the game runs the UpdateActions function, or triggers an action, it will pass target table to the app script. This contains some information about the current target:

Data Description
target.name the internalName of the current target object
target.type TargetType of the current target
target.missionObject Set if the target is a MissionObject.(Check the missionObject table for more details)
target.data Set for data targets. Contain s a table with more information (see data table for details)
target.interactionType Set on interaction targets, this will contain the interactionType
target.macAddress Set on device targets. Contains the device's MAC address as a string.

If the app's targets list includes TargetType.None, and there is no target selected when UpdateAction() or some action is triggered, the target.name will be an empty string, and target.type is set to TargetType.None.

Built-in functions

Status window

Adding a status window for your app lets you display a small message window for the player near the top left corner of the screen. The windows can have an icon, and a small piece of text. While there's no hard limit on text length (for now, at least), it's best to keep things really short to avoid conflicts with how many other apps the player might be running at the same time, and to allow all of them enough screen space.

The LightMontior app is a good example of using status windows:

OnStateChange = function(state)
        if state == AppState.on then
            --UI.ToggleLightMonitor(true)
            CreateStatusWindow()
            SetStatusIcon(1)
            SetStatusIconColor(Color.LightGrey)
            DisplayStatusWindow(true)
        elseif state == AppState.off then
            --UI.ToggleLightMonitor(false)
            RemoveStatusWindow()
        end
        app.state = state
        SetState(app.state)
    end,

    Update = function()
        -- Get player light value
        lightLevel = Player.GetLightLevel()
        -- change icon color:
        if lightLevel < 0.06 then
            SetStatusIconColor(Color.Green)
        elseif lightLevel < 0.15 then
            SetStatusIconColor(Color.Blue)
        else
            SetStatusIconColor(Color.LightGrey)
        end
        -- update status text
        statusText = string.format("%.1f %%", lightLevel * 100)
        UpdateStatusWindow(statusText)
    end,

So, what happens here is we use OnStateChange() to check if the app has been turned on, and if that's the case then we ask the game to create a status window for the app. It's going to be empty, and invisible, to start with. Next we assign an icon for it (first one form the status icons list in the app definition) and set that icon's color to light grey (which matches nicely with rest of the UI content). When all that is done,. we tell the game to actually show the window.

The, in the Update() function, we check the light level, and based on it we change the status icon's color, and then also parse the light level into a nice format to display to user.

Finally, in OnStateChange() function, we make sure to remove the status window when the app is switched off.

We could just choose to show and hide the window as needed, and in some cases you might want to do that in Update() or something to only display the window part of the time. It's still best to actually remove the window completely when the app is not running.

All the functions to handle status windows are explained better below:

CreateStatusWindow()

This is just a simple instruction for the game that you want your app to have a status window. No further options, the game will create the window for you, with a default Off Grid logo as icon, and no text. The window will be hidden by default.

DisplayStatusWindow()

Use DisplayStatusWindow(true) and DisplayStatusWindow(false) to show and hide the window.

RemoveStatusWindow()

When a window is not needed any longer, for example when the app is switched off, use this to remove it from the UI.

SetStatusIcon()

SetStatusIcon() sets the status window's icon to one of the icons defined in the statusIcons list in your app definition. Keep in mind that unlike most programming languages, Lua lists start from 1, so to se the first icon you'd run SetStatusIcon(1).

SetStatusIconColor()

If you want to change the icon color, SetStatusIconColor() lets you do that. As with everything else in Off Grid scripting, you can either define the color as RGBA values, or use one of our pre-defined colors.

For the best results you should make your original icon image pure white, the color set here will multiply any color values from the icon image with the new color, so anything else than white (or at least grayscale) icon mgiht result in unexpected colors...

using pre-defined color:

SetStatusIconColor(Color.Blue)

using a table to define red, green, blue and alpha values:

SetStatusIconColor({0.2, 0.4, 0.9, 1.0})

App Window

Apps can also open a separate UI window of their own, with a background picture, optional progress bar, and few line's worth of space for displaying some text to the player.

Be aware that there can only be one AppWindow at a time, any attempts to open a new one while some other app is already displaying a window will just be silently ignored.

A good example of this, using a Lua coroutine to add some delays to the output, would be in the FlutterSearch app:

app = {
	name = "FlutterSearch",
	description = "Search the local networks for files with matching Flutter ads cookie",
	targets = { TargetType.Character },
	range = 10.0,
	cost = 5,
	useSpectrum = true,
	appType = AppTypes.Active,
	showInRadial = AppMenuState.target,
	showInAppDock = AppMenuState.byDefault,
	state = AppState.off,
	icon = "FlutterSearch.png",
	iconColor = Color.Cyan,

    OnTrigger =  function(target)
		app.targetCharacter = target
		StartLuaCoroutine(app.DownloadRoutine)
    end,

}

function app:DownloadRoutine()

    -- Get all the target character's data from Spectrum:
    local dataPoints = DataPoints.GetObjectDataPoints(app.targetCharacter.name)

    -- filter for only points with metadata value
    local pointsToSave = {}    
    if #dataPoints > 0 then
        for _, d in ipairs(dataPoints) do
            local metavalue = Player.GetDataPointMetaValue(d)
            if metavalue >= 1 then
                table.insert(pointsToSave, d)
            end
        end
    end

    Apps.Console_OutputLine(app.name, "Searching for files with Flutter ads cookie: " .. app.targetCharacter.name )


    if #pointsToSave > 0 then -- Found some valuable data, let's start "downloading" the files

        -- Open the AppWindow
        CreateAppWindow("FlutterSearch", "FlutterSearch_AppWindowBkg.png")
        ShowAppWindowProgress(true)
        ShowAppWindow()

        local progressPerPoint = 0.5/#pointsToSave
        local progress = 0
        local windowText = "Found Flutter ad profile '" .. app.targetCharacter.name .. "'"

        -- First some filler log messages, and a popup while "working" for next 5 seconds
        Apps.Console_OutputLine(app.name, windowText)
        SetAppWindowContent(windowText)

        for i = 0, 50, 1 do
            progress = progress + 0.1
            SetAppWindowProgress(progress)
            coroutine.yield(0.1)
        end
        
        windowText = windowText .. "\n" .. #pointsToSave .. " files found.</size>"
        SetAppWindowContent(windowText)

        coroutine.yield(1)

        -- Then "download" files one by one
        windowText = windowText .. "\nDownloading files..."
        SetAppWindowContent(windowText)
        for _, d in ipairs(pointsToSave) do
            Apps.Console_OutputLine(app.name, "cp -n //" .. app.targetCharacter.name .. "/" .. d.info.dataPointName .. " ~/Dowloads/" .. d.info.dataPointName)
            Spectrum.SaveDataPoint(d)
            progress = progress + progressPerPoint
            SetAppWindowProgress(progress)
            coroutine.yield(0.1)
        end

        windowText = windowText .. "\nCompleted, closing connection."
        SetAppWindowContent(windowText)
        coroutine.yield(1)
        RemoveAppWindow()

    else  -- no data of any value, say "sorry" and refund the points:
        local noDataText = "Could not find any data for" .. app.targetCharacter.name
        Apps.Console_OutputLine(app.name, noDataText )
        UI.ShowPopUp(PopupType.generic, "FlutterSearch", noDataText, 3)
        Player.RecoverNetPoints(app.cost)
    end

CreateAppWindow("window title", "backgroundImage.png")

Creates the app window, and sets the window's tiutle and background. The window will not be visible at this point.

ShowAppWindowProgress(true|false)

Enable/disable a progress bar at the boittom of the window.

ShowAppWindow()

Calling this will make the app window visible to the player. Typically you'd do this after setting the window content.

SetAppWindowContent("content text")

Sets the content text of the app window.

SetAppWindowProgress(0-1)

Sets the progress bar at the botom of the window (if enabled). Use values from 0 to 1.

RemoveAppWindow()

Hides the app window and removes it.


App state

As explained in the OnStateChange(), to make sure it's always the Lua app that determines what the app does (perhaps excluding some extreme cases like player phone shutting down due to running out of battery etc), nothing on the game side can directly tell the app to change it's state. Instead, the game will ask the Lua app to do so, and the app can then do it, or choose not to. Once you have determined what the app does, you should then communicate that back to the game, using SetState() function.

SetState()

Use SetState(app.state) to set the App's state on the game side to match the state your Lua script is in at the moment. This will be reflected on the app's icon color in AppWheel/RadialUI, and also in player being able to (or not) to use the app.

Apps in your mission and other scripts

Some of the app functionality can be triggered from your other Lua scripts. Most commonly, you'd want use this to make apps available to the player during your mission rather than from the start, but there are of course more creative ways to make use of the API. It's worth remembering, though, that in the end it's the App's Lua script that determines what the app does, anything else can only request it to do something.

Apps API

The full Apps Lua API is documented here.