Posts Tagged hashlink

Posted on Programming

Heaps/Hashlink on Android

I wrote Hexlock using Haxe and Heaps, which went quite well – until I decided to compile it for Android. But it’s there now and we can all relax. Aside from getting Heaps setup to compile on Android, I ran into a few other issues.

  1. Touch event coordinates were wildly inflated

This made it seem like my interaction events weren’t going through at all (because in a sense they were not), but I tracked this back to touch events being multiplied by 10,000. They come in as a value between 0 and 1, and I discovered that they’re multiplied by windowWidth (which makes sense), but then divided by something called TOUCH_SCALE. It turns out I needed to add -D hl_ver=1.12.0 to my compile.hxml file to get it to use the appropriate TOUCH_SCALE.

2. I wanted to trigger vibration on Android

So I did. Click here to if you want to know how: triggering vibration on Android in Heapse/Hashlink.

3. Handling the back button

If you’re playing, the back button should take you back to the level select screen. If you’re at the level select screen, it should do what the back button does on Androids and minimize the app. And now it does! If you want to know how, check out dealing with the Android back button for Heaps/Hashlink.

4. hxd.Save didn’t work out-of-the-box

At this point I was too tired (lazy) to dig into why, so I just implemented my own method of saving using Android shared preferences. It seems to work! If you’d like to learn how, click here: saving data for Android with Heaps/Hashlink.

Again, I am not saying these are the best, correct, or even good ways to handle those issues. They’re just what I did, and they worked for me.

Posted on Programming

Heaps/Hashlink saving data for Android

When I was making Hexlock, I was disappointed but not overly surprised to find that hxd.Save didn’t work out-of-the-box on Android. It’s quite possible that it would have after some simple changes, but at that point I was too tired to dig through source files to try to discover why and what those changes may be.

So I just implemented my own method of saving using Android shared preferences.

The first step was to load and store data differently when compiling for Android, which wasn’t too bad because I already had my own wrapper to handle that. A lot of this will contain code specific to Hexlock, which you can ignore or not at your discretion. Basically I keep a map of levels (JSON strings) to score data, load it once when the game starts, and save the entire map out on a new high score.

public static function loadSaves():Void {
	var defValue = new Map<String, Highscore>(); // The default value
	#if android
		var bytes = Main.load(getInstance().name); // These are hl.Bytes
		var data = @:privateAccess String.fromUTF8(bytes); // Convert it to a String

		// This is 100% overkill but a wise man once said:
		// It is better to just throw in random possible values
		// than to wait for Android Studio to finish compiling and check 
		// what they actually are.
		if (data.length == 0 || data == "null" || data == null) { 
			// Welp, at least we have the default value
			getInstance().data = defValue;
			return;
		}

		// This code is basically taken from hxd.Save
		var obj : Dynamic = haxe.Unserializer.run(data);

		// set all fields that were not set to default value (auto upgrade)
		if( defValue != null && Reflect.isObject(obj) && Reflect.isObject(defValue) ) {
			for( f in Reflect.fields(defValue) ) {
				if( Reflect.hasField(obj, f) ) continue;
				Reflect.setField(obj, f, Reflect.field(defValue,f));
			}
		}
            
		getInstance().data = obj; // That's our data now            
	#else
		// If it's not for Android, just use hxd.Save
		getInstance().data = Save.load(defValue, getInstance().name);
	#end
}
public static function save(level:Level, highschore:Highscore) {
	getInstance().data.set(Json.stringify(level), highscore); // Add the score
	#if android
		var data = haxe.Serializer.run(getInstance().data);
		Main.save(getInstance().name, data);
	#else
		Save.save(getInstance().data, getInstance().name);
	#end
}

For both save() and loadSaves() hxd.Save is used, except when compiling to Android, because I added -D android to my compile-to-c.hxml. There may already be a hlNative one #shrug. I didn’t see one, and adding -D android was easy enough.

You can see from above that the code requires the function Main.save(String, String) and Main.load(String); getInstance().name is just a constant string identifier like ‘my_cool_highscore_file’.

Since those functions are only referenced when compiling for Android, they only need to be defined when compiling for Android, which is convenient because hl.Bytes isn’t available on non-hashlink targets.

And in Main.hx:

#if android
@:hlNative("Java_io_heaps_android_HeapsActivity")
public static function save(name:String, data:String) {}
#end

#if android
@:hlNative("Java_io_heaps_android_HeapsActivity")
public static function load(name:String):hl.Bytes { var i = 4; return hl.Bytes.fromValue("null", i); }
#end

Who knows if I needed a body for load(String) – not me – but it worked, so I didn’t look into it further.

When compiling to C code for Hashlink, calls to the function Main.save() get replaced with Java_io_heaps_android_HeapsActivity_save(), which I defined in my jni.c1 file, and of course an equivalent transformation happens to Main.load().

JNIEXPORT vbyte* JNICALL Java_io_heaps_android_HeapsActivity_load(vstring *name) {
	(*jvm)->AttachCurrentThread(jvm, &thisEnv, 0); //this is important to avoid threading errors
	jclass cls = (*thisEnv)->FindClass(thisEnv, "io/heaps/android/HeapsActivity");
	jmethodID method = (*thisEnv)->GetStaticMethodID(thisEnv, cls, "loadData", "(Ljava/lang/String;)Ljava/lang/String;");

	char *cname = hl_to_utf8(name->bytes);
	__android_log_print(ANDROID_LOG_DEBUG, "JNI.c", "loading... %s", cname);

	// Is this necessary? Who knows! Better safe than seg fault though.
	char *buf = strdup(cname);
	jstring jstrBuf = (*thisEnv)->NewStringUTF(thisEnv, buf);

	jstring result = (*thisEnv)->NewStringUTF(thisEnv, "null");
	if (method > 0)
		result = (jstring) (*thisEnv)->CallStaticObjectMethod(thisEnv, cls, method,jstrBuf);
    
	char *cresult =   (*thisEnv)->GetStringUTFChars(thisEnv, result, 0);
	__android_log_print(ANDROID_LOG_DEBUG, "JNI.c", "returned... %s",cresult);


	free(buf); // Free the string
	return cresult;
}

JNIEXPORT jstring JNICALL Java_io_heaps_android_HeapsActivity_save(vstring *name, vstring *data) {
	(*jvm)->AttachCurrentThread(jvm, &thisEnv, 0); //this is important to avoid threading errors
	jclass cls = (*thisEnv)->FindClass(thisEnv, "io/heaps/android/HeapsActivity");
	jmethodID method = (*thisEnv)->GetStaticMethodID(thisEnv, cls, "saveData", "(Ljava/lang/String;Ljava/lang/String;)V");

	char *cname = hl_to_utf8(name->bytes);
	char *cdata = hl_to_utf8(data->bytes);

	char *bname = strdup(cname);
	char *bdata = strdup(cdata);

	__android_log_print(ANDROID_LOG_DEBUG, "JNI.c", "saving %s: %s", bname, bdata);

	jstring jname = (*thisEnv)->NewStringUTF(thisEnv, bname);
	jstring jdata = (*thisEnv)->NewStringUTF(thisEnv, bdata);

	if (method > 0)
		(*thisEnv)->CallStaticVoidMethod(thisEnv, cls, method, jname, jdata);

	free(bname);
	free(bdata);
}

To find out more about where thisEnv and jvm come from, check out triggering vibration on Android from Heaps/Hashlink.

Haxe strings come through Hashlink as vstrings, and the JNI uses jstrings, so you can see me converting between the two, above. Onload I just send back the char* instead of dealing with transforming it back into a vstring, and handle that in Haxe (which you can see even further above). Is this a memory leak? Who knows! Not me. But if it is, oh well – the load function is only called once during the apps lifetime.

As you can see, the C code assumes there will be saveData() and loadData() functions in io.heaps.android.HeapsActivity, and there will be, because we’re about to add them!

static public void saveData(String name, String data) {
	SharedPreferences sharedPref = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);
	SharedPreferences.Editor editor = sharedPref.edit();
	editor.putString(name, data);
	editor.apply();
}

static public String loadData(String name) {
	SharedPreferences sharedPref = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);
	return sharedPref.getString(name, "null");
}

Above you can see that I just use name for both the SharedPreferences file and for the key in it. Whoops, that’s embarrassing. I could have used another string defined in the java, or just omitted specifying a name completely but it works and that’s good enough for me.

If you want to find out more about the files I’m referencing (including a Git repo hosting them), you can do so here: Hello World; Heaps on Android.