You are here

trent's blog

The official license post

For a while now here at Nooskewl, we've been using a license called the Nooskewl "Give it your own license, license" for our free and open source software releases. Not much has been said about the license because I haven't thought about it all that much. But to clear things up (hopefully), here is what it means.

We used to license our stuff as public domain, and I became aware that "public domain" was not a real thing in some countries. Now, I'm not a lawyer, so this could be completely bogus, but our license is meant to give you all the freedoms you'd ever want from a public domain license, but with actual license terms so that it can be used in those countries where public domain doesn't exist.

The terms of the license are very, very simple. The content DOES have a license, but the license permits you to change the license to whatever license you want. That's a lot of license. To write the license out in detail we need one bullet point:

1) This software is distributed under the Nooskewl "Give it your own license,
license", which gives the licensee the right to relicense the software under
any of their own terms, or any existing license they desire, having no effect
on anyone else's terms.

Now if I'm completely wrong on the legality of this, forgive me. I haven't researched it. I'm not knowledgable in the area of law. But until proven faulty, that is the Nooskewl "Give it your own license, license."

Monster RPG 2 FL Studio files

I still have some of the original music I made for Monster RPG 2 as FLP (Fruity Loops/FL Studio) files. These are all released under the Nooskewl "Give it your own license, license." 5 songs in there. Hope you enjoy it.

http://www.nooskewl.com/team/trent/m2_flps.zip

Download files/updates from your game

At the time of this writing, I'm working on Monster RPG 2 1.11, and one of the new things will be shipping with "regular" quality music, but giving the user the option of downloading the full quality soundtrack. Obviously this involves transferring some files over a network. I've written a simple FTP client in Java before, and I decided that's too complex for this purpose. I've decided to go with TFTP (Trivial File Transfer Protocol) instead. By "trivial" they mean very easy to implement, and it is. So what I've written is a bunch of functions that work together to do the following:

  • Download a list of files to be downloaded
  • Process the list and download each file listed in it

That's the gist of it. I've only tested it on OS X thus far. I want it to be fairly reliable so I do a lot of error checking. The only thing I haven't done yet is an MD5 sum. Instead, the code just checks file sizes. If this turns out to be a problem during testing (or, God forbid, after deployment), I'll have to implement something better. I'm going to post the code here now. It's plain C and does use Allegro 5, however the Allegro parts could fairly easily be removed. The only thing from Allegro needed here is the file size function, al_fsize.

Another note, this code is set up to stop downloading when the "stop" boolean variable is set to true. As a test, Allegro is used to spawn a thread and set stop to true after 30 seconds. You'll want to remove that and use "stop" more appropriately if you modify this source.

Finally, this test program will download the whole Monster RPG 2 soundtrack. TFTP is comparatively slow so expect it to take 10-15 minutes on a good connection if you let it complete.

#include <allegro5/allegro.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef WINPC
#include <windows.h>
#include <winsock2.h>
#define my_shutdown(a, b) closesocket(a)
#define SHUT_RDWR 0xf008a7
#else
#include <sys/socket.h>
#include <netdb.h>
#define INVALID_SOCKET -1
#define my_shutdown shutdown
#endif
#include <sys/time.h>

#define SERVER "nooskewl.com"
#define PORT "69"
#define BLKSIZE 32768
#define BLKSIZE_S "32768"

#define NUM_FILES 42 // FIXME: change this as needed
#define EXPECTED_LIST_SIZE (NUM_FILES * 81) // 60 chars filename 20 chars length + 1 \n

#define DOWNLOAD_PATH "." // FIXME: change me

static volatile bool stop = false;

static int sock = -1;
static struct sockaddr saddr;
static socklen_t saddr_len;

static int get(char *buf, int len)
{
	int n = recvfrom(sock, buf, len, 0, &saddr, &saddr_len);
	if (n < 1) return 0;
	return n;
}

static int put(const char *buf, int len)
{
	int n = sendto(sock, buf, len, 0, &saddr, saddr_len);
	return n;
}

static bool connect_to_server(void)
{
#ifdef WINPC
	WSAData crap;
	WSAStartup(2, &crap);
#endif

	struct addrinfo hints, *res;

	// first, load up address structs with getaddrinfo():

	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;

	int r;

	if ((r = getaddrinfo(SERVER, PORT, &hints, &res)) != 0) {
		return false;
	}
	
	// make a socket:

	sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	if (sock == INVALID_SOCKET) {
		freeaddrinfo(res);
		return false;
	}

	saddr_len = res->ai_addrlen;
	memcpy(&saddr, res->ai_addr, sizeof(struct sockaddr));

	freeaddrinfo(res);

	return true;
}

static void shutdown_connection(void)
{
	shutdown(sock, SHUT_RDWR);
}

// return size
static int get_rrq(char *buf, const char *filename)
{
	buf[0] = 0;
	buf[1] = 1;
	buf += 2;
	strcpy(buf, filename);
	buf[strlen(filename)] = 0;
	buf += strlen(filename)+1;
	strcpy(buf, "octet");
	buf[strlen("octet")] = 0;
	buf += strlen("octet")+1;
	strcpy(buf, "blksize");
	buf[strlen("blksize")] = 0;
	buf += strlen("blksize")+1;
	char *bsize = BLKSIZE_S;
	strcpy(buf, bsize);
	buf[strlen(bsize)] = 0;

	return strlen(filename) + strlen("octet") + strlen("blksize") + strlen(bsize) + 6;
}

static void mkack(char *buf)
{
	buf[0] = 0;
	buf[1] = 4;
}

int get16bits(const char *buf)
{
	unsigned char *b1 = (unsigned char *)buf;
	unsigned char *b2 = (unsigned char *)buf+1;
	return ((int)(*b1) << 8) | (int)(*b2);
}

static int download_file(const char *filename)
{
	char fn[1000];
	sprintf(fn, "%s/%s", DOWNLOAD_PATH, filename);
	FILE *f = fopen(filename, "wb"); // FIXME
	char buf[4+BLKSIZE];
	int sz, total_size = 0;
	int last_blocknum = -1;
	
	if (!connect_to_server()) {
		printf("couldn't connect\n");
		fclose(f);
		return -1;
	}

	sz = get_rrq(buf, filename);
	if (put(buf, sz) != sz) {
		printf("read request fail\n");
		fclose(f);
		shutdown_connection();
		return -1;
	}

	// read option acknowledgement
	sz = get(buf, 2+strlen("blksize")+1+strlen(BLKSIZE_S)+1);
	if ((sz != (2+strlen("blksize")+1+strlen(BLKSIZE_S)+1)) || get16bits(buf) != 6) {
		fclose(f);
		shutdown_connection();
		return -1;
	}

	buf[0] = 0;
	buf[1] = 4;
	buf[2] = 0;
	buf[3] = 0;
	if (put(buf, 4) != 4) {
		fclose(f);
		shutdown_connection();
		return -1;
	}

	printf("downloading %s\n", filename);

	while (1) {
		if (stop) {
			fclose(f);
			shutdown_connection();
			return -1;
		}
		fd_set fdset;
		FD_ZERO(&fdset);
		FD_SET(sock, &fdset);
		struct timeval tv;
		tv.tv_sec = 60;
		tv.tv_usec = 0;
		if (select(sock+1, &fdset, 0, 0, &tv) == 0) {
			fclose(f);
			shutdown_connection();
			return -1;
		}
		sz = get(buf, 4+BLKSIZE);
		if (sz != 4+BLKSIZE) {
			break;
		}
		int blocknum = get16bits(buf+2);
		if (last_blocknum == -1 || blocknum != last_blocknum) {
			fwrite(buf+4, BLKSIZE, 1, f);
			total_size += BLKSIZE;
		}
		last_blocknum = blocknum;
		mkack(buf);
		if (put(buf, 4) != 4) {
			fclose(f);
			shutdown_connection();
			return -1;
		}
	}

	if (sz > 4) {
		fwrite(buf+4, sz-4, 1, f);
		total_size += sz - 4;
		mkack(buf);
		if (put(buf, 4) != 4) {
			fclose(f);
			shutdown_connection();
			return -1;
		}
	}

	fclose(f);
	shutdown_connection();

	return total_size;
}

void download_list(char **filenames, int *lengths)
{
	int i = 0;

	while (filenames[i]) {
		int len = download_file(filenames[i]);
		if (stop) {
			return;
		}
		if (len != lengths[i])
			continue;
		i++;
	}
}

bool download_all(void)
{
	char fn[1000];
	sprintf(fn, "%s/%s", DOWNLOAD_PATH, "list.txt");
	FILE *f = fopen(fn, "r");
	size_t read;
	char buf[100];
	int sz;
	char **filenames = NULL;
	int *lengths = NULL;
	int count = 0;
	while ((read = fread(buf, 80, 1, f)) == 1) {
		fseek(f, 1, SEEK_CUR);
		buf[80] = 0;
		if (sscanf(buf, "%s %d", fn, &sz) != 2) {
			fclose(f);
			return false;
		}
		ALLEGRO_FILE *f2 = al_fopen(fn, "rb");
		if (f2) {
			int sz2 = al_fsize(f2);
			al_fclose(f2);
			if (sz2 == sz) {
				continue;
			}
		}
		count++;
		if (filenames == NULL) {
			filenames = malloc(2 * sizeof(char *));
			lengths = malloc(2 * sizeof(int));
		}
		else {
			filenames = realloc(filenames, (count+1) * sizeof(char *));
			lengths = realloc(lengths, (count+1) * sizeof(int));
		}
		filenames[count-1] = strdup(fn);
		lengths[count-1] = sz;
	}
	if (filenames != NULL) {
		filenames[count] = NULL;
		lengths[count] = 0;

		printf("Downloading %d files\n", count);

		download_list(filenames, lengths);

		free(filenames);
		free(lengths);
	}
	else {
		printf("Already done!\n");
	}

	fclose(f);

	return true;
}

static const char *get_status(float *percent)
{
	*percent = 0.0f;

	char fn[1000];
	sprintf(fn, "%s/%s", DOWNLOAD_PATH, "list.txt");

	ALLEGRO_FILE *f = al_fopen(fn, "r");

	if (f == NULL || al_fsize(f) != EXPECTED_LIST_SIZE) {
		if (f) al_fclose(f);
		goto nuthin;
	}

	size_t read;
	char buf[100];
	int sz;
	int count = 0;
	while ((read = al_fread(f, buf, 80)) == 80) {
		al_fseek(f, 1, ALLEGRO_SEEK_CUR);
		buf[80] = 0;
		if (sscanf(buf, "%s %d", fn, &sz) != 2) {
			if (count == 0) {
				al_fclose(f);
				goto nuthin;
			}
			al_fclose(f);
			goto partial;
		}
		ALLEGRO_FILE *f2 = al_fopen(fn, "rb");
		if (f2) {
			int sz2 = al_fsize(f2);
			al_fclose(f2);
			if (sz2 != sz) {
				if (count == 0) {
					al_fclose(f);
					goto nuthin;
				}
				goto partial;
			}
		}
		else {
			if (count == 0) {
				al_fclose(f);
				goto nuthin;
			}
			goto partial;
		}
		count++;
	}

	if (count == NUM_FILES) {
		al_fclose(f);
		*percent = 1.0f;
		return "Complete";
	}

partial:
	al_fclose(f);
	*percent = (float)count / NUM_FILES;
	return "Partially downloaded";

nuthin:
	return "Nothing downloaded";
}

void *test_thread(void *arg)
{
	(void)arg;
	al_rest(30);
	stop = true;
	return NULL;
}

int main(void)
{
	al_init();

	al_run_detached_thread(test_thread, NULL);
	
	int len = download_file("list.txt");
	if (len != EXPECTED_LIST_SIZE) {
		printf("Connection error\n");
		return 1;
	}

	const char *status;
	float percent;
	status = get_status(&percent);
	printf("Status: %s (%d percent)\n", status, (int)(100*percent));

	bool ret = download_all();
	if (ret) printf("Success!\n");
	else printf("Error!\n");

	return 0;
}

Simplest HTML5 audio playlist player

Get ready for the code. It'll look exactly like this. Just drop it into a directory with OGG and MP3 files (same names, different extensions.) It uses some PHP and requires Javascript to be enabled, and of course an HTML5 browser.

<html>
<head>
	<title>audio</title>
	<?php
	# song list

	$dir = opendir(".");

	print "<script type=\"text/javascript\">\n";
	print "\t\tvar songlist = new Array()\n";

	$i = 0;
	$songs = array();

	while ($entry = readdir($dir)) {
		if (strpos($entry, "ogg") === FALSE) {
			continue;
		}
		$entry = preg_replace("/.ogg/", "", $entry);
		$songs[$i] = $entry;
		print("\t\tsonglist[$i] = \"$entry\"\n");
		$i++;
	}

	closedir($dir);

	print "\t</script>\n";

	?>

	<script type="text/javascript">
		var num_tracks = songlist.length
		var curr_track = 0
		var ext = "ogg"
		var audio
		var paused = true
		var played_once
	
		function is_safari()
		{
			var ua = navigator.userAgent.toLowerCase(); 
			if (ua.indexOf('safari') != -1){ 
				if (ua.indexOf('chrome') > -1) {
					return false;
				}
				else {
					return true;
				}
			}
			return false;
		}
		
		function start()
		{
			if (is_safari()) {
				ext = "mp3"
			}

			audio = document.createElement("audio")
			setTimeout("update()", 1000)
		}

		function update()
		{
			if (paused == false) {
				if (audio.ended) {
					next()
				}
			}
			setTimeout("update()", 1000)
		}

		function next()
		{
			curr_track++
			curr_track %= num_tracks

			audio.setAttribute("src", "" + songlist[curr_track] + "." + ext)
			audio.load()
			audio.play()

			document.getElementById("currsong").innerHTML = songlist[curr_track]
		}

		function jump(n)
		{
			paused = false
			played_once = true
			curr_track = n-1
			if (curr_track < 0) {
				curr_track = num_tracks-1
			}
			next()
		}

		function play()
		{
			if (played_once) {
				paused = false
				audio.play()
			}
			else {
				jump(0)
			}
		}

		function pause()
		{
			if (paused) {
				return
			}
			paused = true
			audio.pause()
		}
	</script>
</head>
<body onload="start()">
	<a href="javascript:play()">Play</a> | <a href="javascript:pause()">Pause</a>

	<p>Current song: <span id="currsong">farmer</span></p>

	<?php
		print "<ol>\n";
		for ($i = 0; $i < sizeof($songs); $i++) {
			print "<li><a href=\"javascript:jump($i)\">$songs[$i]</a></li>\n";
		}
		print "</ol>\n";
	?>
</body>
</html>

Android EGL lock errors on 2.1 (eclair)

Over the past ten days I've had my first experiences with Android development, and it was a pleasure. For the most part. Even the day long debugging sessions. But one problem in particular haunted me the whole time, until today. There is a known issue with Android 2.1, but to my dismay, patched firmware was the only solution I could find. Being a noob to Android and due to the lack of any ROMs for my device, I thought I was toast, not willing to take the risk of creating some firmware myself as a rookie.

Then, after losing most hope of solving the problem, which manifests itself as log errors (see below) and a horribly flickering screen, I stumbled upon a partial fix. The log errors are still there, but the flickering is gone!

03-16 18:27:32.186 E/gralloc ( 4625): handle 0x14f2f0 already locked for write
03-16 18:27:32.186 E/        ( 4625): EGL: Failed to lock buffer
03-16 18:27:32.205 E/gralloc ( 4625): handle 0x14f2f0 not locked
03-16 18:27:32.205 E/        ( 4625): EGL: Failed to unlock buffer

And it was what seemed to be the most innocent thing imaginable causing me this grief: glClear! I haven't decided if this is correct or not, but it looks an awful lot like glClear is causing buffers to be swapped as if eglSwapBuffers was called! Anyway let me cut to the chase. I fixed this problem by conditionally (when Android 2.1 is the OS and the clear is on the backbuffer) using two screen sized triangles to overwrite the backbuffer, while saving and restoring the transformation, making it a simple orthographic projection with identify view transform temporarily. Hey, it gets the job done without the nasty flicker, even if it isn't 100% ideal. I hope anyone struggling with this problem finds this blog post while they still have hair left on their head!

Linear filtering in software

Somebody asked recently how to do an OpenGL type linear filter when using software graphics. Linear filtering is very easy and this approximation works very well, albeit a bit slow and unoptimized. This is a small example that takes an input, "src.png", and outputs "out.png" scaled 2x up with filtering. Code is worth a thousand words so here it is.

#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>

int main(int argc, char **argv)
{

	al_init();
	al_init_image_addon();

	ALLEGRO_BITMAP *src_image = al_load_bitmap("src.png");
	int start_w = al_get_bitmap_width(src_image);
	int start_h = al_get_bitmap_height(src_image);
	int width = start_w * 2;
	int height = start_h * 2;
	ALLEGRO_BITMAP *dst_image = al_create_bitmap(width, height);
	al_set_target_bitmap(dst_image);

	// width/height are desired
	// start_w/start_h are what the image starts at
	for (int y = 0; y < height; y++) {
		float pixy = ((float)y / height) * start_h;
		for (int x = 0; x < width; x++) {
			float pixx = ((float)x / width) * start_w;
			ALLEGRO_COLOR a = al_get_pixel(src_image, pixx-0.5, pixy-0.5);
			ALLEGRO_COLOR b = al_get_pixel(src_image, pixx+0.5, pixy-0.5);
			ALLEGRO_COLOR c = al_get_pixel(src_image, pixx-0.5, pixy+0.5);
			ALLEGRO_COLOR d = al_get_pixel(src_image, pixx+0.5, pixy+0.5);
			ALLEGRO_COLOR result = al_map_rgba_f(
				(a.r+b.r+c.r+d.r) / 4,
				(a.g+b.b+c.g+d.g) / 4,
				(a.b+b.g+c.b+d.b) / 4,
				(a.a+b.a+c.a+d.a) / 4
			);
			al_put_pixel(x, y, result);
		}
	}

	al_save_bitmap("out.png", dst_image);
}

And here is the before and after:

EDIT:

A closer result to what a GPU will produce with its linear filtering can be had by changing a few lines, to add weights to the pixels. Thanks to Paul Suntsov for this:

#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <cmath>

ALLEGRO_COLOR interpolate(ALLEGRO_COLOR c1, ALLEGRO_COLOR c2, float weight)
{
	return al_map_rgba_f(
		c1.r*weight + c2.r*(1-weight),
		c1.g*weight + c2.g*(1-weight),
		c1.b*weight + c2.b*(1-weight),
		c1.a*weight + c2.a*(1-weight)
	);
}

int main(int argc, char **argv)
{

	al_init();
	al_init_image_addon();

	ALLEGRO_BITMAP *src_image = al_load_bitmap("src.png");
	int start_w = al_get_bitmap_width(src_image);
	int start_h = al_get_bitmap_height(src_image);
	int width = start_w * 2;
	int height = start_h * 2;
	ALLEGRO_BITMAP *dst_image = al_create_bitmap(width, height);
	al_set_target_bitmap(dst_image);

	// width/height are desired
	// start_w/start_h are what the image starts at
	for (int y = 0; y < height; y++) {
		float pixy_f = ((float)y / height) * start_h;
		float pixy = floor(pixy_f);
		float weight_y = 1.0 - (pixy_f - pixy);
		for (int x = 0; x < width; x++) {
			float pixx_f = ((float)x / width) * start_w;
			float pixx = floor(pixx_f);
			ALLEGRO_COLOR a = al_get_pixel(src_image, pixx, pixy);
			ALLEGRO_COLOR b = al_get_pixel(src_image, pixx+1, pixy);
			ALLEGRO_COLOR c = al_get_pixel(src_image, pixx, pixy+1);
			ALLEGRO_COLOR d = al_get_pixel(src_image, pixx+1, pixy+1);
			float weight_x = 1.0 - (pixx_f - pixx);

			ALLEGRO_COLOR ab = interpolate(a, b, weight_x);
			ALLEGRO_COLOR cd = interpolate(c, d, weight_x);
			ALLEGRO_COLOR result = interpolate(ab, cd, weight_y);

			al_put_pixel(x, y, result);
		}
	}

	al_save_bitmap("out.png", dst_image);
}

And the result:

Now all of them together:

From left to right: Nearest neighbor, original method in this post, new method in this post.

What was the first "Allegro 5" game ever made?

I believe the answer is Rain Runner, which I made with a very early DirectX-only version of Allegro 4.9, which became 5.0 and now 5.1 (development branch.) Not many people have seen it, and not many people need to see it, because it's very very simple. But for those who like their history and are Allegro users, this might be interesting. The original, unmodified Windows binary and source code lie somewhere on this site, but today I got the idea to update the game to run with the latest version of Allegro 5.1.

There were four "main" differences between that old version of Allegro and the current (as of this post) version. 1) ALLEGRO_COLOR was always passed as a pointer back then. For example: ALLEGRO_COLOR c; al_map_rgb(&c, r, g, b);. 2) al_current_time(), which is now dubbed al_get_time() but still has the old alias, used to return a long, but now returns a double. Before it returned a time in milliseconds, and now it returns a fractional number in seconds. 3) The font addon was the first addon IIRC and we had no standard practices for addons. You would include "a5font.h" or something like that, and function in the addon were prefixed with a5font_. Nowadays we use "allegro5/allegro_font.h" and all core addons use the al_ prefix. 4) The final difference was blending. We used to include a color in the al_set_blender call, and the default blender was ALLEGRO_ONE, ALLEGRO_ZERO, and there was no premultiplied alpha. Now to tint something you call al_draw_tinted_* where before that tint color was passed to a blender call.

After changing those things, and a few other minor things, the game works with Allegro 5.1. Here is the source code and data.

rain_runner-a5.1.zip

recent_posts 1.3 - works with cache

The recent_posts module has been updated to properly work with caching. I'm running varnish with the expire module and it works great (I think, still need to inspect varnish logs a little.) Here it is.

recent_posts-1.3.zip

recent_posts drupal module 1.2

I found a big bug in the recent posts module I recently created (and posted here.) The date of last post and last person to post were usually wrong. Here is the corrected version as version 1.2. Unzip it into your Drupal modules directory, then you can configure it (well, what little there is to configure) under Admin->Configuration->Recent Posts. After that just add it as a block somewhere.

Recent posts module 1.2

Hide reply links from comments in Drupal 7

I searched around for how to do what is in the title, and never found anything working for Drupal 7. I did find enough information to piece together the answer though. Without much explanation, here is how I did it.

Background: I based my theme off of the 'seven' theme. I got a comment.tpl.php from modules/comment in my Drupal installation and modified it in a few ways including this change. The code starts off like this:

  <?php print render($content['links']) ?>

This is only part of it of course, near the end. This prints a list of links for the comment (edit, delete, reply, etc.) The change is really simple:

  <?php
    hide($content['links']['#links']['comment-reply']);
    print render($content['links'])
  ?>

We simply use 'hide' to get rid of the link with the name comment-reply. Users can still comment by clicking 'Add new comment' on the original post, and, if enabled, using the comment form at the bottom of each topic.

Pages