Posted on Art

Risk of Rain 2 (intro and outro)

It was immediately clear to me that something was missing in my new career as a professional streamer. And, no – I know what you’re thinking – it wasn’t a dedicated following full of enchanted viewers. Chasing viewers is for people who don’t already have them, and I have a well-deserved zero, thank you very much.

It wasn’t skill.

No, what I was missing was something to take the edge off the jarring starts and stops that wrapped each stream – and I knew just what to do.

The first step was to figure out how on Earth to use After Effects. Whether I succeeded at that remains hotly debated, but I did manage to cobble together an intro video.

Realizing only then that future streams would feel unacceptably lopsided, I panicked and threw together a quick closing video as well.

Please enjoy with my blessing.

Posted on Art

My name is Gorf.

You, loyal reader, may recall that I have spoken from time to time of Percy Animé, my D&D character. In fact, a picture I painted of him sits framed behind me as I type.

Well, truth be told, I also painted a picture of different character—Gorf: a small frog-man with a penchant for stealing swords from his handsomest companions. And, yes, he is wearing a turtle-shell shield strapped to his back.

His catch phrase, as I’m sure you can imagine is: I am Gorf. Tell me your name and I shall tall you mine.

Disclaimer: Gorf was a created and played by acclaimed game developer Daniel Garma, who informs me that he is a member of the “Grung”.

Posted on Business

Hourible: the time tracking tool I use

I made a new online tool!

Okay so new is a bit of a stretch. It’s based on an internal tool I’ve used at altered effect for years now, but I’ve cleaned that up and now it’s available for public use:

Time tracking is the horrible. (see what I did there?) But does it have to be? I don’t know.

It’s one of the great mysteries of life, and something I worry we’ll never know the answer to.

Long story long, I wasn’t happy with any of the time tracking solutions I found so I settled on something quick and easy – using Google calendar. I was already comfortable using it, and I it was surprisingly intuitive to just drag events to the appropriate size in order to keep track of how much time I spend on projects.

It actually works wonderfully, but when it comes for invoicing you need to add up the hours.

Or do you? Trick quesion – you do, but Hourible does it for you!

Over whatever time frame you like. Plus, you can easily filter the events. Since I use Logipar any chance I get, Hourible supports logical operators in the filter string. It sums up your hours for you, and you can take a deeper look to see how they’re broken down.

Of course, you still need to make the invoices then. Which you can do manually if you prefer, or Hourible can generate them automatically with the click of a button. Which… is a lot less work for me than making them manually. It’s quite convenient actually.

All that and Hourible is free as you want it to be!

Give it a try if you want, especially if you regularly work on multiple projects or for various clients. Maybe it’ll help.

hourible.com

Posted on crafting

A new handle on knife

See what I did there? You’re welcome.

The handle on one of my mother’s kitchen knives was deteriorating so I decided to “fix” it.

See? Deteriorating. Why didn’t you just believe me?

So I took some measurements…

Just look at those scribbles. Those are some quality measurements.

And using those measurements I made a 3D model in Fusion 360.

     

Which I printed a bunch of times on my SnapMaker 2.0! With adjustments here and there until I was satisfied with the fit.

Don’t mind the teeth or circuitry in the background. Focus instead on all the knife scales. Can you tell which ones are 3d printed?
Here’s the knife without its scales. It looks so naked.
The 3D print worked pretty well! If only I wasn’t worried about.. you know, thermo plastic being near heat sources and a perfect surface area for bacteria growth.

Once I was happy with the model, I CNCed it out, also using my SnapMaker 2.0, on a piece of test wood. Yeah it does CNC and 3D printing, so what? (Also laser cutting.)

The SnapMaker 2.0 is pretty cool.
They seemed fine.

They the test CNCing went pretty well, so I did it again – but this time from using some black walnut from a tree that grew on her childhood home.

The brass rods fit! I taped up the knife blade for “safety”, he said trying to distract from how dull the blade actually is.

Then I cut the brass rods down to size with a hacksaw, and started peening them. Oh also, I glued them in (and the scales to tang) with some resin.

Peening has begun.

It turned out pretty alright, I think! I did some sanding and used some wood filler to plug a gap, which… I should have used a different colour, but I happened to have one on-hand.

Also I made a quick leather sleeve for it because I started to feel embarrassed by the painters tape.

Here’s the pointy end!
Here’s the stubby end.
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.

Posted on Programming

Dealing with the Android back button for Heaps/Hashlink

I wanted the back button on Android to work in a reasonable way for Hexlock: 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.

It turns out that different versions of Android send different key codes with the event when you hit the back button, (and I also wanted to support Esc and Backspace on keyboard), so I matched a few of them.

var g = new LevelSelect();
sceneManager.requestMainMenu = function() {
	var s = sceneManager.switchScene(g);
	if (s == g) {
		trace("Already on the main menu, exit game");
		exitapp();
	}
}

hxd.Window.getInstance().addEventTarget(function(event : hxd.Event) {
	switch(event.kind) {
		case EKeyUp: 
			// backspace and escape; the last one is what my android sends for back
			if (event.keyCode == 8 || event.keyCode == 27 || event.keyCode == 1270)
				sceneManager.requestMainMenu();
		case _: // Match the rest
	}
});

Then I added a static function exitapp() to my Main class, the idea being that it would do nothing unless I was compiling for Android. (I added -D android to my compile-to-c.hxml for just this purpose). There may already be a hlNative one, but who knows at this point. I didn’t see one, and adding -D android was easy enough.

#if android
@:hlNative("Java_io_heaps_android_HeapsActivity")
#end
public static function exitapp() : Void { }

When compiling to C code for Hashlink, calls to the function Main.exitapp() get replaced with Java_io_heaps_android_HeapsActivity_exitapp(), which I defined in my jni.c1 file.

JNIEXPORT void JNICALL Java_io_heaps_android_HeapsActivity_exitapp() {
	(*jvm)->AttachCurrentThread(jvm, &thisEnv, 0);
	jclass cls = (*thisEnv)->FindClass(thisEnv, "io/heaps/android/HeapsActivity");
	jmethodID method = (*thisEnv)->GetStaticMethodID(thisEnv, cls, "callbackExit", "()V");
	if (method > 0)
		(*thisEnv)->CallStaticVoidMethod(thisEnv, cls, method);
}

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

As you can see the C code assumes there is an callbackExit() function in io.heaps.android.HeapsActivity, and there is, because I added one!

public static void callbackExit() {
	Intent intent = new Intent(Intent.ACTION_MAIN);
	intent.addCategory(Intent.CATEGORY_HOME);
	instance.startActivity(intent);
}

And that seemed to work! Even better, it didn’t crash.

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.