Skip to content

About

a framework focus on mobile

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

extension-mobiletools

A small, mobile-focused toolkit for Lime / OpenFL projects. It exposes common native mobile features through one cross-platform Haxe API, and adds a scripting and modding system on top.

Status: early base (0.2.0). The API is small on purpose and will grow. The scripting, modding and mod installing code has automated tests (see Tests). The rest of the Haxe code typechecks for desktop, Android, iOS and html5, and the Android Java code compiles. Neither native backend has been run on a real device yet, and the iOS Objective-C++ has not been compiled at all.

Features

Feature Android iOS Other targets
Runtime permissions ✅ ✅ always granted
Toast messages ✅ ✅¹ no-op
Vibration ✅ ✅² no-op
Haptics + patterns ✅ ✅ no-op
Keep screen on ✅ ✅ no-op
Fullscreen ✅ ✅³ no-op
Safe area insets ✅ ✅ all zero
File picker ✅ ✅ returns null
Device info ✅ ✅ defaults
Storage folders ✅ ✅ working folder
Scripting (hscript) ✅ ✅ ✅
Modding ✅ ✅ ✅ (not html5)
Mods from .zip ✅ ✅ ✅ (not html5)

¹ iOS has no native toast, so it is drawn as an overlay on the key window. ² iOS only has a fixed-length system vibration: the duration is ignored and cancelVibration() does nothing. ³ iOS hides the status bar and lets the home indicator fade out. There is no navigation bar to hide.

Unsupported targets never crash: calls are silent no-ops, so you don't need #if guards.

Installation

haxelib git extension-mobiletools https://github.com/TheMoonCrew/extension-mobiletools

Then add it to your project.xml:

<haxelib name="extension-mobiletools" />

hscript is installed as a dependency. It is only compiled into your app if you use mobiletools.scripting or mobiletools.mods.

Usage

import mobiletools.MobileTools;

MobileTools.showToast('Level saved!');              // short toast
MobileTools.showToast('Connection lost', LONG);     // long toast
MobileTools.vibrate(50);                            // 50 ms (Android)
MobileTools.setKeepScreenOn(true);
MobileTools.setFullscreen(true);                    // hide the system bars

var info = MobileTools.getDeviceInfo();
trace('${info.manufacturer} ${info.model} - API ${info.apiLevel}');

if (MobileTools.isSupported)
    trace('Native backend active');

Two runnable examples live in samples/: basic (minimal Lime) and showcase (a touch-friendly OpenFL app that exercises every feature, including mods).

Haptics

Two levels: the platform's own effects for UI feedback, and fully custom patterns.

import mobiletools.Haptics;

// Semantic effects: they feel native on each platform
Haptics.selection();             // a picker or switch changed
Haptics.impact(MEDIUM);          // LIGHT, MEDIUM, HEAVY, SOFT or RIGID: a button press, a collision
Haptics.notification(SUCCESS);   // SUCCESS, WARNING or ERROR

// Named patterns: built in, registered by you, or provided by a mod
Haptics.playNamed('heartbeat');  // false if there is no such pattern

// Your own pattern
Haptics.play(new HapticPattern('victory')
    .tap(0, 1, 0.9)              // time (ms), intensity, sharpness
    .tap(120, 0.7, 0.9)
    .hold(300, 250, 0.6)         // time, duration, intensity
    .ramp(600, 400, 0.6, 0));    // time, duration, from, to: a fade out

Haptics.stop();                  // cut a pattern short

Patterns

A HapticPattern is a list of events, each with a time, a duration (0 is a short tap), an intensity and a sharpness (0 soft, 1 crisp). Besides tap, hold and ramp you can repeat(count, interval) what you have so far, and make copies with scaled(factor) (intensity) and stretched(factor) (speed).

Platform How a pattern plays
iOS with Core Haptics Exact timing, intensity and sharpness.
iOS without Core Haptics Approximated with the impact generators, one tap per event.
Android 8+ with amplitude control A waveform with a strength per moment. Sharpness only shortens taps. Overlapping events keep the strongest.
Android without amplitude control Plain on and off.

Check what the device supports with Haptics.capabilities (supported, amplitudeControl, sharpnessControl).

Patterns are checked as they are built (at most 256 events and 30 seconds, no NaN, intensities clamped to 0-1), because they can come from mods.

JSON

Patterns load from and save to JSON, which is also how mods ship them:

{
    "name": "hello-wave",
    "events": [
        {"time": 0,   "duration": 120, "intensity": 0.3, "sharpness": 0.2},
        {"time": 140, "duration": 120, "intensity": 0.6},
        {"time": 480, "intensity": 1.0, "sharpness": 1.0}
    ]
}

Only time is required: duration defaults to 0, intensity to 1 and sharpness to 0.5.

var pattern = HapticPattern.fromJson(json);
trace(pattern.toJson(true));

The pattern library

Names resolve through layers, highest priority first: mods, then your own patterns, then the built in ones.

Haptics.register('hit', new HapticPattern().tap(0).tap(80, 0.6));   // your own
Haptics.playNamed('hit');
Haptics.unregister('hit');
Haptics.patternNames();    // everything available right now

Built in patterns: double-tap, triple-tap, knock, pulse, heartbeat, buzz, alarm, rumble, explosion, ramp-up, ramp-down. Registering (or a mod providing) a pattern with one of these names replaces it, and removing it brings the original back.

The user's settings

Haptics.enabled = false;     // a master switch for your settings screen: silences everything, mods included
Haptics.intensity = 0.5;     // scales custom patterns (the platform effects have a fixed strength)

Scripts cannot change either of these.

Needs the VIBRATE permission on Android, which the library declares for you.

Permissions

import mobiletools.Permissions;

// Ask (shows the system prompt only if the OS still allows it)
Permissions.request(CAMERA, function(status)
{
    if (status.isGranted())
        startCamera();
    else if (status == DENIED_PERMANENTLY)
        Permissions.openSettings(); // the only way to change it now
});

// Check without prompting
var status = Permissions.check(MICROPHONE);

// Several at once, results come back in the same order
Permissions.requestMany([CAMERA, MICROPHONE], function(results) trace(results));

Available permissions:

Permission Android iOS
CAMERA CAMERA Camera
MICROPHONE RECORD_AUDIO Microphone
LOCATION ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION Location, while in use
NOTIFICATIONS POST_NOTIFICATIONS (Android 13+, otherwise always granted) Notifications
IMAGES READ_MEDIA_IMAGES (13+), else READ_EXTERNAL_STORAGE Photo library
VIDEO READ_MEDIA_VIDEO (13+), else READ_EXTERNAL_STORAGE Photo library (same as IMAGES)
AUDIO READ_MEDIA_AUDIO (13+), else READ_EXTERNAL_STORAGE Media / music library
STORAGE READ/WRITE_EXTERNAL_STORAGE up to Android 12L, always granted after always granted
EXTERNAL_STORAGE Up to Android 10 the storage prompt, Android 11+ "All files access" always granted
BLUETOOTH BLUETOOTH_CONNECT + BLUETOOTH_SCAN (Android 12+, else always granted) Bluetooth (iOS 13+)
CONTACTS READ_CONTACTS Contacts
CALENDAR READ_CALENDAR + WRITE_CALENDAR Calendar (full access)
MOTION ACTIVITY_RECOGNITION (Android 10+, else always granted) Motion & Fitness
BIOMETRICS Availability only, see below Availability only
LOCATION_ALWAYS ACCESS_BACKGROUND_LOCATION (request LOCATION first) Location, always
TRACKING always granted App Tracking Transparency (iOS 14+)
PHONE READ_PHONE_STATE always granted
NEARBY_WIFI NEARBY_WIFI_DEVICES (Android 13+, else always granted) always granted
BODY_SENSORS BODY_SENSORS always granted (HealthKit is separate)
OVERLAY Draw over other apps (settings screen) always granted
IGNORE_BATTERY_OPTIMIZATIONS Run without battery limits (system dialog) always granted
EXACT_ALARM SCHEDULE_EXACT_ALARM (settings screen on Android 12+, else always granted) always granted
WRITE_SETTINGS Change system settings (settings screen) always granted
INSTALL_PACKAGES Install other apps (settings screen on Android 8+) always granted

Settings screen permissions. EXTERNAL_STORAGE (Android 11+), OVERLAY, IGNORE_BATTERY_OPTIMIZATIONS, EXACT_ALARM, WRITE_SETTINGS and INSTALL_PACKAGES are not granted in a dialog: the request opens the matching system screen and finishes when the user comes back. Because that screen can always be opened again, a refusal is DENIED, never DENIED_PERMANENTLY. Google Play restricts apps that declare MANAGE_EXTERNAL_STORAGE, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SCHEDULE_EXACT_ALARM and REQUEST_INSTALL_PACKAGES, so only use them when you really need to.

LOCATION_ALWAYS needs LOCATION first: from Android 11 the system refuses background location until foreground location is granted, then sends the user to a settings screen. On iOS it upgrades "while using" to "always", which the system offers only once.

BIOMETRICS is different from the rest: neither platform lets an app ask for biometrics, so request() never shows anything. check() tells you whether they can be used:

Status Meaning for BIOMETRICS
GRANTED Available and enrolled
DENIED Supported, but nothing is enrolled (or temporarily locked)
DENIED_PERMANENTLY iOS only: Face ID was refused for this app
RESTRICTED No biometric hardware, or the OS is too old

Actually authenticating is not part of this library yet.

Status Meaning
NOT_DETERMINED Never asked, request() will prompt
GRANTED Allowed (also returned when the OS has no runtime permission for it)
DENIED Denied, but the system can prompt again (Android)
DENIED_PERMANENTLY Won't prompt again, use openSettings() (every iOS denial)
RESTRICTED Blocked by the system, e.g. parental controls (iOS)

Callbacks run on the main thread. If no prompt is needed (already granted or permanently denied) the callback runs immediately.

Declare what you use

The library does not declare permissions for you, so your app only ships the ones it needs.

Android - add to your project.xml:

<config:android permission="android.permission.CAMERA" />                <!-- CAMERA -->
<config:android permission="android.permission.RECORD_AUDIO" />          <!-- MICROPHONE -->
<config:android permission="android.permission.ACCESS_FINE_LOCATION" />  <!-- LOCATION -->
<config:android permission="android.permission.ACCESS_COARSE_LOCATION" />
<config:android permission="android.permission.POST_NOTIFICATIONS" />    <!-- NOTIFICATIONS (Android 13+) -->
<config:android permission="android.permission.READ_MEDIA_IMAGES" />     <!-- IMAGES (Android 13+) -->
<config:android permission="android.permission.READ_MEDIA_VIDEO" />      <!-- VIDEO (Android 13+) -->
<config:android permission="android.permission.READ_MEDIA_AUDIO" />      <!-- AUDIO (Android 13+) -->
<config:android permission="android.permission.READ_EXTERNAL_STORAGE" /> <!-- IMAGES, VIDEO, AUDIO, STORAGE (Android 12L and older) -->
<config:android permission="android.permission.WRITE_EXTERNAL_STORAGE" /><!-- STORAGE (Android 12L and older) -->
<config:android permission="android.permission.MANAGE_EXTERNAL_STORAGE" /><!-- EXTERNAL_STORAGE (Android 11+) -->
<config:android permission="android.permission.BLUETOOTH_CONNECT" />     <!-- BLUETOOTH (Android 12+) -->
<config:android permission="android.permission.BLUETOOTH_SCAN" />
<config:android permission="android.permission.READ_CONTACTS" />         <!-- CONTACTS -->
<config:android permission="android.permission.READ_CALENDAR" />        <!-- CALENDAR -->
<config:android permission="android.permission.WRITE_CALENDAR" />
<config:android permission="android.permission.ACTIVITY_RECOGNITION" />  <!-- MOTION (Android 10+) -->
<config:android permission="android.permission.USE_BIOMETRIC" />         <!-- BIOMETRICS -->
<config:android permission="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- LOCATION_ALWAYS (Android 10+) -->
<config:android permission="android.permission.READ_PHONE_STATE" />      <!-- PHONE -->
<config:android permission="android.permission.NEARBY_WIFI_DEVICES" />   <!-- NEARBY_WIFI (Android 13+) -->
<config:android permission="android.permission.BODY_SENSORS" />          <!-- BODY_SENSORS -->
<config:android permission="android.permission.SYSTEM_ALERT_WINDOW" />   <!-- OVERLAY -->
<config:android permission="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- IGNORE_BATTERY_OPTIMIZATIONS -->
<config:android permission="android.permission.SCHEDULE_EXACT_ALARM" />  <!-- EXACT_ALARM (Android 12+) -->
<config:android permission="android.permission.WRITE_SETTINGS" />        <!-- WRITE_SETTINGS -->
<config:android permission="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- INSTALL_PACKAGES -->

Without the manifest entry Android denies the request immediately. Runtime permissions need target SDK 23 or higher.

iOS - the app crashes if you request a permission without its usage description. Lime merges every extra *-Info.plist placed next to your app's <app file>-Info.plist, so create e.g. Permissions-Info.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>NSCameraUsageDescription</key>              <string>Why you need the camera</string>
	<key>NSMicrophoneUsageDescription</key>          <string>Why you need the microphone</string>
	<key>NSLocationWhenInUseUsageDescription</key>   <string>Why you need your location</string>
	<key>NSPhotoLibraryUsageDescription</key>        <string>Why you need your photos and videos</string>
	<key>NSAppleMusicUsageDescription</key>          <string>Why you need your music</string>
	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>Why you need your location in the background</string>
	<key>NSUserTrackingUsageDescription</key>        <string>Why you track the user</string>
	<key>NSBluetoothAlwaysUsageDescription</key>     <string>Why you need Bluetooth</string>
	<key>NSContactsUsageDescription</key>            <string>Why you need your contacts</string>
	<key>NSCalendarsUsageDescription</key>           <string>Why you need your calendar</string>
	<key>NSCalendarsFullAccessUsageDescription</key> <string>Why you need your calendar (iOS 17+)</string>
	<key>NSMotionUsageDescription</key>              <string>Why you need motion data</string>
	<key>NSFaceIDUsageDescription</key>              <string>Why you use Face ID</string>
</dict>
</plist>

and copy it into the Xcode project folder (MyApp = your <app file="...">):

<template path="Permissions-Info.plist" rename="MyApp/Permissions-Info.plist" if="ios" />

Notifications, STORAGE and EXTERNAL_STORAGE need no plist key.

Storage folders

var mods = MobileTools.getDirectory(APP_EXTERNAL) + '/mods';
Directory Android iOS
INTERNAL App private folder, never visible to the user Application Support
APP_EXTERNAL Android/data/<package>/files, no permission needed Documents
SHARED Root of shared storage (needs EXTERNAL_STORAGE to write) Documents (no shared storage on iOS)

On desktop all three are the working directory. Paths have no trailing slash, and are empty when unavailable.

Fullscreen and the safe area

MobileTools.setFullscreen(true);        // hide the system bars
MobileTools.isFullscreen;               // true

var safe = MobileTools.getSafeArea();   // pixels on each edge covered by a notch, camera hole or system bar
hud.y = safe.top;
pauseButton.x = stage.stageWidth - pauseButton.width - safe.right;
  • Android: immersive mode. The status and navigation bars are hidden, a swipe from the edge brings them back for a moment, and the app draws next to the notch too (Android 9+). It is applied again when the app returns from the background or a dialog closes (a permission prompt, for example).
  • iOS: the status bar is hidden, the home indicator fades out, and system edge swipes need a second swipe so they don't fire by accident during play. Leave UIViewControllerBasedStatusBarAppearance at its default. If you set it to NO, only the status bar can be hidden.

getSafeArea() returns {top, right, bottom, left} in pixels of the game's view: only the parts the game actually draws under are counted. It changes when the device rotates and when fullscreen is turned on or off, so read it again in your resize handler. It also covers the rounded corners on iOS.

Pick one way of going fullscreen. On Android, setFullscreen() and Lime's <window fullscreen="true" /> (or window.fullscreen) work on the same system bars, and SDL hides them again a moment after setFullscreen(false) if Lime's fullscreen is on.

Scripts can read mobile.isFullscreen() and mobile.getSafeArea(), but only the app can change fullscreen.

File picker

import mobiletools.FilePicker;

FilePicker.pick(['zip', 'json'], function(path)
{
    if (path != null)
        trace(sys.io.File.getContent(path));
});

This opens the system picker: the Files app on iOS, the document picker on Android (Downloads, Drive, SD cards, other apps). The callback gets the path of a private copy of the chosen file, or null if the user cancelled. No permission is needed on either platform. The copy is kept in the app's cache and replaced by the next pick, so move it if you want to keep it. Android can't filter on every file type, so check what you got. On other targets the callback receives null straight away.

Scripting

mobiletools.scripting.Script runs hscript code (Haxe-like syntax, interpreted at runtime) in a sandbox.

import mobiletools.scripting.Script;

var script = Script.fromString('
    var score = 0;
    function addPoints(n) { score += n; mobile.selection(); return score; }
', 'level1.hx');

script.call('addPoints', [10]);   // 10
script.get('score');              // 10
script.has('onUpdate');           // false, so optional hooks are safe to call

Errors are thrown as a String that names the script and function.

What scripts can use. Only what they are given: Std, Math, StringTools, Json (parse, stringify, prettyStringify), mobile and haptics:

mobile.showToast('Hi');                mobile.showLongToast('Hi');
mobile.vibrate(100);                   mobile.selection();
mobile.impact('heavy');                // light, medium, heavy, soft, rigid
mobile.notification('success');        // success, warning, error
mobile.getDeviceInfo();                mobile.isMobile / mobile.isSupported
mobile.isFullscreen();                 mobile.getSafeArea();   // {top, right, bottom, left} in pixels
mobile.platform                        // "android", "ios", "desktop" or "html5"

Haptics from scripts (haptics) can play named patterns and build new ones. Builder methods return the builder, so calls chain, and trailing arguments are optional:

haptics.play('heartbeat');                       // by name, false if unknown
haptics.pattern().tap(0, 1, 0.9).hold(100, 300, 0.5).ramp(400, 300, 0.5, 0).play();
haptics.define('my-hit', haptics.pattern().tap(0).tap(80, 0.6));   // save it for later
haptics.fromJson(text).play();   haptics.fromData({events: [{time: 0}]}).play();
haptics.has('my-hit');   haptics.names();   haptics.stop();
haptics.impact('heavy');   haptics.notification('success');   haptics.selection();

tap(time, [intensity], [sharpness]), hold(time, duration, [intensity], [sharpness]), ramp(time, duration, from, to, [steps], [sharpness]), repeat(count, interval), then play(), duration() or toJson(). The limits above apply, and Haptics.enabled and Haptics.intensity are out of reach.

Give a script more by passing globals to Script.fromString, or ModManager.expose() for mods.

Sandbox. Reflect, Type, Sys and file access are never exposed, and new SomeClass() only works for a short allow list (StringBuf, EReg, Date, StringMap, IntMap). Plain hscript would let a script create any class in your app, so this is replaced. Open one up with Script.allowClass('haxe.ds.ObjectMap'), but the class must be part of your app: Haxe removes classes nothing uses, so mark it @:keep.

Things to know:

  • There is no time limit: a script with an endless loop freezes the app.
  • Declare a top-level var before the functions that use it.
  • Lime projects get hscriptPos from this library's include.xml, so script errors have line numbers. Outside Lime, define it yourself (-D hscriptPos). On C++ (Android, iOS) it is needed for errors inside loops to be reported, see ScriptInterp.
  • Reflection is slower and less portable on some targets: scripts call functions with all their arguments, since optional ones are not reliable.

Modding

mobiletools.mods.ModManager finds mods in folders, enables them, runs their scripts and serves their files.

import mobiletools.mods.ModManager;

ModManager.init();                              // looks in <APP_EXTERNAL>/mods and restores what was enabled
ModManager.onLog = (source, msg) -> trace('[$source] $msg');
ModManager.onError = (source, msg) -> trace('Mod problem: $source: $msg');

for (mod in ModManager.mods)
    trace('${mod.name} ${mod.enabled}');

ModManager.enable('cool-colors');               // enables its dependencies first
ModManager.disable('cool-colors');
ModManager.move('cool-colors', 0);              // later mods win shared files

var path = ModManager.findFile('images/bg.png'); // from the highest priority enabled mod, or null
var text = ModManager.readText('data/levels.json');

// in your game loop
ModManager.update(elapsed);                     // calls onUpdate(elapsed) in every enabled mod
ModManager.call('onScore', [100]);              // calls your own hook in every enabled mod

Which mods are enabled and their order are saved between launches.

A mod

hello-mod/
├── mod.json
├── scripts/
│   └── main.hx
├── haptics/
│   └── hello-wave.json  haptic patterns, see below
└── images/bg.png        any files you want to replace or add
{
    "id": "hello-mod",
    "name": "Hello Mod",
    "version": "1.0.0",
    "author": "Ana",
    "description": "Says hello.",
    "dependencies": ["base-pack"],
    "scripts": ["scripts/main.hx"],
    "platforms": ["mobile"]
}

Only name is required. id defaults to the folder name, and scripts defaults to everything in scripts/.

platforms limits where the mod can be enabled: any of android, ios, mobile (both), desktop and html5. Leave it out and the mod works everywhere. On other platforms the mod is still listed, with mod.error explaining why it is off. To try a mobile only mod on desktop, set ModManager.platform = 'android' before ModManager.init().

// scripts/main.hx
var taps = 0;

function onLoad()   { mod.log('loaded'); mobile.showToast('Hello!'); }
function onUnload() { mod.log('bye'); }
function onUpdate(elapsed) { }
function onTap()    { taps++; mobile.impact('light'); return taps; }

// app events, called for you with Lime
function onPause()  { }                  // the app went to the background
function onResume() { }                  // and came back
function onResize(width, height) { }     // rotation, split screen, window resize

onPause, onResume and onResize come from Lime's window events, which ModManager.init() connects to. To trigger them yourself, set ModManager.followAppEvents = false before init() and call ModManager.pause(), resume() and resize(width, height). onResume can also run once when the app starts.

Besides mobile and anything you expose, each script gets a mod object: mod.id, mod.name, mod.version, mod.log(msg), mod.exists(file), mod.readText(file), mod.readJson(file) and mod.list(folder). These only reach inside the mod's own folder: absolute paths, .. and symbolic links pointing outside are refused.

Haptic patterns. Every haptics/*.json of an enabled mod becomes available to Haptics.playNamed() and to every script, named by the name inside the file or else the file name. A mod with the same name as a built in pattern replaces it, the later mod wins between mods, and disabling a mod gives everything back. Patterns a script saves with haptics.define() belong to its mod and disappear with it. A broken pattern file is reported to onError and skipped, it doesn't stop the mod.

A mod that fails to load (bad script, missing or circular dependency) stays disabled with mod.error set, and never stops the others. A script that throws at runtime is reported to onError and skipped.

Installing mods on a phone

On a phone, mods usually arrive as a .zip in Downloads or a chat app, and the mods folder is hard to reach (on Android 11+ even file managers can't open Android/data). So let the user pick the zip instead:

ModManager.importFromDevice(function(mod)
{
    if (mod != null)
        ModManager.enable(mod.id);       // or let the user switch it on
});

ModManager.install('/path/to/cool-colors.zip');   // the same, from a file you already have
ModManager.uninstall('cool-colors');              // disables it (and mods needing it) and deletes its folder

importFromDevice() opens the file picker and installs the chosen file. The archive can hold the mod folder (cool-colors/mod.json, ...) or just its contents (mod.json at the top). When there is no id, the folder name, or else the archive name, becomes the id.

Installing is all or nothing: the mod is unpacked into a hidden folder first and only moved into place when every file was checked. Installing a mod that is already there updates it in its current folder, and keeps it enabled if it was. A new mod starts disabled, in <installRoot>/<id>. Archives come from strangers, so install() refuses:

  • entries that would land outside the mod folder (.., absolute paths), and never creates symbolic links
  • more than ModManager.maxInstallFiles files (10 000) or more than ModManager.maxInstallSize bytes unpacked (256 MB). Sizes are checked before anything is unpacked, and data larger than its entry claims is refused
  • damaged data (every file is checked against its CRC), encrypted and ZIP64 archives
  • archives with no mod.json, or with several mods
  • a new mod whose folder already exists with something else in it, which is left alone

Problems go to onError, and the method returns null. Mods are installed into ModManager.installRoot, which defaults to <APP_EXTERNAL>/mods. uninstall() only deletes mods directly inside one of the roots, and it removes symbolic links without following them.

Where mods live

ModManager.init() looks in <APP_EXTERNAL>/mods, which needs no permission. To also use a folder users can reach from a file manager or computer, ask for shared storage:

ModManager.useSharedStorage('MyGame', function(ok) trace(ok)); // <shared storage>/MyGame/mods

This requests EXTERNAL_STORAGE, so declare it as described above. On iOS set UIFileSharingEnabled in your Info.plist to see the Documents folder in the Files app. You can add any other folder with ModManager.addRoot(path).

Mods do not exist on html5, which has no file system.

Giving mods something to do

Mods can only touch what you expose:

ModManager.expose('game', {
    addScore: function(points:Int) score += points,
    getLevel: function() return currentLevel
});

Keep it small, and validate arguments: scripts come from files the user downloaded.

Tests

haxe tests/test.hxml

Runs three test programs (378 checks) on Haxe's interpreter, inside a temporary folder: ModTest (scripting, sandbox, mods), HapticTest (patterns, waveform conversion, the pattern library, the Haptics class with a recording backend, the script API and mod provided patterns) and MobileTest (fullscreen and the safe area with a fake backend, the file picker fallback, the zip reader, installing, updating and uninstalling mods and what they refuse, platforms and the app event hooks). They also pass compiled to neko and to C++, and with hscriptPos defined.

Project layout

src/mobiletools/          Haxe API (MobileTools, Permissions, Haptics, HapticPattern, ...)
src/mobiletools/backend/  Platform backends (IBackend, AndroidBackend, IOSBackend, NullBackend)
src/mobiletools/scripting/ hscript sandbox (Script, ScriptInterp, ScriptApi)
src/mobiletools/mods/     Mod discovery, management and installing (ModManager, Mod, ModInfo, ModArchive)
java/com/mobiletools/     Android native code, called through JNI
project/ios/              iOS native code (Objective-C++), compiled by hxcpp
include.xml               Lime build config (Java sources, iOS frameworks)
samples/basic/            Minimal Lime sample
samples/showcase/         Touch UI demo app (OpenFL)
tests/                    Scripting and modding tests

Adding a backend

  1. Implement mobiletools.backend.IBackend.
  2. Select it for your target in MobileTools.createBackend(), or install it at runtime with MobileTools.setBackend() (handy in tests).
  3. Add any native sources / permissions to include.xml.

License

MIT - see LICENSE.

About

a framework focus on mobile

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Contributors

Languages