Skip to content

Instantly share code, notes, and snippets.

@Y-Less
Last active August 29, 2015 14:26
Show Gist options
  • Select an option

  • Save Y-Less/7aae8535302a100a904b to your computer and use it in GitHub Desktop.

Select an option

Save Y-Less/7aae8535302a100a904b to your computer and use it in GitHub Desktop.
Menu DSL brief tutorial

Operator overloading for code simplification

Introduction

In this tutorial, we will develop a library for creating SA:MP menus. We don't bother with the code for actually displaying the result to the end-user, because it is both simple and irrelevant to the main point. We then look at one trick used to make the code vastly simpler to read and write. First, imagine that we have the following top-level "Buy Things" menu displayed to a user:

 Buy Things 
------------
Weapons
Vehicles
Clothes

If they select "Vehicles", a second menu is opened in its place:

 Vehicles
----------
Banshee
Infernus
Boats
Tank

This menu hierarchy will from now on be represented like so:

Buy Things
	Weapons
		Minigun
		Desert Eagle
		Knuckle Dusters
	Vehicles
		Banshee
		Infernus
		Boats
			Predator
			Reefer
		Tank
	Clothes
		Hat
		(Gloves)
		Cap

There are no gloves, so it is a disabled item, represented here by surrounding brackets.

For example, from this hierarchy, you can see that selecting "Boats" from the vehicles menu opens yet another menu to select a boat. This menu system is the one that will be used in all examples (minus, as previously mentioned, display and processing).

Code

A first pass might look like:

new
	Menu:menuBuyThings = CreateMenu("Buy Things"),
	Menu:menuWeapons   = CreateMenu("Weapons"),
	Menu:menuVehicles  = CreateMenu("Vehicles"),
	Menu:menuBoats     = CreateMenu("Boats"),
	Menu:menuClothes   = CreateMenu("Clothes");

AddChildMenu(menuBuyThings, menuWeapons);
AddChildMenu(menuBuyThings, menuVehicles);
AddChildMenu(menuBuyThings, menuClothes);

AddMenuItem(menuWeapons, "Minigun");
AddMenuItem(menuWeapons, "Desert Eagle");
AddMenuItem(menuWeapons, "Knuckle Dusters");

AddMenuItem(menuVehicles, "Banshee");
AddMenuItem(menuVehicles, "Infernus");
AddChildMenu(menuVehicles, menuBoats); // Add the third level menu.
AddMenuItem(menuVehicles, "Tank");

AddMenuItem(menuBoats, "Predator");
AddMenuItem(menuBoats, "Reefer");

AddMenuItem(menuWeapons, "Hat");
AddMenuItem(menuClothes, "Gloves", false); // Disabled (enabled is default).
AddMenuItem(menuClothes, "Cap");

Given suitable implementations of those functions, that is a standard method of constructing menus (or indeed any hierarchy). However, we can go better.

However, this code is bulky (lots of repetition), unclear (in that the hierarchy is not at all obvious), and error-prone (spot the intentional bug). It would be nice to create something much more obvious, something along the lines of:

Buy Things
	+ (Weapons
		+ Minigun
		+ Desert Eagle
		+ Knuckle Dusters)
	+ (Vehicles
		+ Banshee
		+ Infernus
		+ (Boats
			+ Predator
			+ Reefer)
		+ Tank)
	+ (Clothes
		+ Hat
		- Gloves
		+ Cap)

That code isn't QUITE possible, but something very close is!

Operators

Pawn has operator overloading for tagged variables. We can exploit this to reduce code. Menus already have a tag of Menu:, but lets give each element a tag too, and convert the strings currently in use to a number (because strings can't have operators):

static stock gLastString[64];

MenuItem:StoreAndTagString(str[])
{
	// Store the string for later.
	strcpy(gLastString, str);
	// Return a tag, the data isn't important (but could be).
	return MenuItem:0;
}

And some macros for smaller code:

#define E StoreAndTagString

Now we write the actual operators. When you try and do:

new
	Num:a = Num:1,
	Num:b = Num:7,
	Num:c = a + b;

You can't, because you can't add Num:s, even though the look like numbers. You need to define an operator to do it, which could do anything:

Num:operator+(Num:left, Num:right)
{
	return Num:(_:a + 2 * _:b);
}

Now c will equal 15 because of the custom addition (with tag overrides to use the default addition operator within that code).

Because operators can do anything, we can use them to construct menus:

Menu:operator+(Menu:parent, Menu:child)
{
	AddChildMenu(parent, child);
	return parent;
}

With that you can "add" menus together, making the right-hand one a child of the left one, and returning the parent (very important):

new
	Menu:menuWeapons   = CreateMenu("Weapons"),
	Menu:menuVehicles  = CreateMenu("Vehicles"),
	Menu:menuBoats     = CreateMenu("Boats"),
	Menu:menuClothes   = CreateMenu("Clothes"),
	Menu:menuBuyThings =
		CreateMenu("Buy Things")
		+ menuWeapons
		+ menuVehicles
		+ menuClothes;

Because the same parent menu is returned each time, the additions can be chained.

You can also define operators for different combinations of tags:

Menu:operator+(Menu:parent, MenuItem:child)
{
	// The data value doesn't matter, only the tag here.
	#pragma unused child
	AddMenuItem(parent, gLastString);
	return parent;
}

Menu:operator-(Menu:parent, MenuItem:child)
{
	// A disabled item instead.
	#pragma unused child
	AddMenuItem(parent, gLastString, false);
	return parent;
}

That is actually all the code that is required. Suddenly, we can write:

new
	Menu:menuWeapons   =
		CreateMenu("Weapons")
		+ E("Minigun")
		+ E("Desert Eagle")
		+ E("Knuckle Dusters"),
	Menu:menuBoats     =
		CreateMenu("Boats")
		+ E("Predator")
		+ E("Reefer"),
	Menu:menuClothes   =
		CreateMenu("Clothes")
		+ E("Hat")
		- E("Gloves")
		+ E("Cap"),
	Menu:menuVehicles  =
		CreateMenu("Vehicles")
		+ E("Banshee")
		+ E("Infernus")
		+ menuBoats
		+ E("Tank"),
	Menu:menuBuyThings =
		CreateMenu("Buy Things")
		+ menuWeapons
		+ menuVehicles
		+ menuClothes;

Or, using brackets, get even closer to the original clear layout:

new
	Menu:menuBuyThings = CreateMenu("Buy Things")
		+ (CreateMenu("Weapons")
			+ E("Minigun")
			+ E("Desert Eagle")
			+ E("Knuckle Dusters"))
		+ (CreateMenu("Vehicles")
			+ E("Banshee")
			+ E("Infernus")
			+ (CreateMenu("Boats")
				+ E("Predator")
				+ E("Reefer"))
			+ E("Tank"))
		+ (CreateMenu("Clothes")
			+ E("Hat")
			- E("Gloves")
			+ E("Cap"))
		;

Obviously many details have been skipped over here, and further improvements to readability and flexibility can be made, but this was just a brief introduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment