Tutorial 8 (Presets) - Aerll/rpp Wiki

In this tutorial we will learn more about the preset feature and create a simple preset for placing borders. We will also see, how we can use it with different tilesets.

Presets

Presets are nothing more than reusable runs and you can think of them as templates. We can essentially create a run, where the user can inject the values of his choice. We already used one such preset FillObjects, which created a run based on the objects we passed to it. Internally it loops through all of them and creates a rule for each.

Unlike everything we wrote up until now, presets are not executed by the program and we need to explicitly ask for that by calling it. In a way it's similar to variables, where we first introduce a name, and then this name refers to some value. The difference is that presets aren't values, they are blocks of code, therefore we need to use a slightly more sophisticated syntax.

Signature

In r++ all functions have a signature, which is used to identify them. It's basically the full name of a function, which includes the name itself and a list of parameter types. Here are some examples:

Function(int i)
Function(coord c)
Function(int x, int y)
Function(int x, int y, int z)
AnotherFunction()
YetAnotherOne(range r, coord c)

As you may have noticed, there are 4 signatures with the same name, yet they would be perfectly valid to use. If we write down the full name for each of these, we get the following:

name: Function -> types: int
name: Function -> types: coord
name: Function -> types: int int
name: Function -> types: int int int

We can see that the types are different, therefore all signatures are different.

But it doesn't end there, signature is also unique to the kind of function we're using. We can use the same signature for a preset, function and a nested function. Here's a minimal example (which you don't have to understand at this point) showcasing this:

#include "base.r"

AutoMapper("");

preset test()
    warning("preset.test()");
end
function->null test() nested(test)
    warning("test()");
    invoke(nested);
end
nested function->null test.test()
    warning("test.test()");
end

preset.test();
test().test();

Even though all 3 functions have the same signature test(), the program has no problem distinguishing them and displays 3 warnings preset.test(), test() and test.test().

Creating a preset

To create a preset we write:

preset signature
    ...
end

Let's break it down:

  • every preset starts with the preset keyword followed by a signature and ends with the end keyword
  • signature - consists of preset's name and a list of parameters
  • ... - this is where we write our usual r++ code, it will be executed when the preset is called

Let's try a simple example, in which we're gonna take our very first automapper from tutorial 1 and wrap it in a preset:

preset Sweep()
    Insert(0);
end

Now when we call it, it will generate a clearing run for us.

We could make it a little better and allow the user of our preset to choose which tile to clear. In that case our preset can take an int and clear only tiles with the given index:

preset Sweep(int tile)
    Replace(tile, 0);
end

When we call this preset, we are free to pass any number to it and it will be assigned to tile. This also can cause problems. What if the user passes some invalid number like -1. We can prevent this and validate the input. Let's throw an error when tile is not in range 1 to 255:

preset Sweep(int tile)
    if (not (tile >= 1 and tile <= 255))
        error("'tile' is out of range.");
    end
    Replace(tile, 0);
end

Calling a preset

We already know how to call a preset, since we used FillObjects on several occasions, but as a reminder, here's how we would call both versions of the above Sweep preset:

preset.Sweep(); // calls 'preset Sweep()'
preset.Sweep(1.N); // calls 'preset Sweep(int tile)'

Depending on the values passed, different preset gets executed.

Important thing to note here, is that the values don't necessarily need to be of the same type. As long as the program is able to convert them, we're fine. For example we could pass a coord to our preset:

preset.Sweep([1, 0].N);

This can however cause some unexpected results in cases where the program has no way of knowing which function we mean to call. Suppose we have 2 presets:

preset SweepAt(int tile, coord at)
    Insert(0).If(IndexAt(at).Is(tile));
end

preset SweepAt(coord at, int tile)
    Insert(0).If(IndexAt(at).Is(tile));
end

This may seem nice, the user can pass his values in any order and the code will just work:

preset.SweepAt(1, [0, 0]); // calls 'preset SweepAt(int tile, coord at)'
preset.SweepAt([0, 0], 1); // calls 'preset SweepAt(coord at, int tile)'

Great. Now let's imagine, that we have some object and we want to remove its anchor from the map using our SweepAt preset:

object o = Indices(1);
preset.SweepAt(o.anchor, [0, 0]); // calls `preset SweepAt(coord at, int tile)`

And here we have an oopsie. We clearly meant to call preset SweepAt(int tile, coord at), yet we ended up calling the other preset.

Why is that? This is due to the types of values we passed. You may think that the anchor is an int, however surprise, it's a coord. The program is looking for the best match for each of the values and prioritizes those signatures. Since our first value is a coord, the better match here is obviously a coord, so now preset SweepAt(coord at, int tile) is considered a better candidate. Second value can be converted to an int, therefore this signature wins the contest.

Example

In this example we will be writing something similar to the grass_main automapper from tutorial 4.2. This time however, we won't be doing it for any specific tileset.

Our goal is to create a preset, which will automatically generate a run for borders. What we need is 13 values: a filler, 4 walls, 4 outer corners and 4 inner corners. Let's write them down as parameters:

preset Border(
    int fill,
    int wall:top, int wall:right, int wall:bottom, int wall:left,
    int outer:topLeft, int outer:topRight, int outer:bottomRight, int outer:bottomLeft,
    int inner:topLeft, int inner:topRight, int inner:bottomRight, int inner:bottomLeft
)

end

Note: it's usually helpful to write a comment inside of a preset in order to document it. Things such as what it does and the meaning of each parameter is very useful to others.

Now that our preset is all set and ready, we can write our run. We're essentially gonna borrow a part of the code from the GrAss example. But before we do that, we should think about what could go wrong here. As we know, all of the values come from the outside, therefore we should make sure they're all valid. If any of the values are not in range [0-255], we throw an error:

if (s:debug)
    array int values = util:ArrayInt(
        fill, wall:top, wall:right, wall:bottom, wall:left,
        outer:topLeft, outer:topRight, outer:bottomRight, outer:bottomLeft,
        inner:topLeft, inner:topRight, inner:bottomRight, inner:bottomLeft
    );
    for (i = 0 to values.last)
        if (values[i] < 0 or values[i] > 255)
            error("preset.Border -> values must be in range [0-255].");
        end
    end
end

This will prevent the user from passing incorrect values, while also informing him about the mistake.

Note: s:debug is a flag used in base.r and base.p, which enables error checking. It is important to include it, since it allows the user to turn it off and speed up execution.

Now that the values are validated we can write all of the rules:

// fill everything
Insert(fill);

// insert walls
Insert(wall:top).If(IndexAt([0, 0]).IsWall(top));
Insert(wall:right).If(IndexAt([0, 0]).IsWall(right));
Insert(wall:bottom).If(IndexAt([0, 0]).IsWall(bottom));
Insert(wall:left).If(IndexAt([0, 0]).IsWall(left));

// insert outer corners
Insert(outer:topLeft).If(IndexAt([0, 0]).IsOuterCorner(topLeft));
Insert(outer:topRight).If(IndexAt([0, 0]).IsOuterCorner(topRight));
Insert(outer:bottomRight).If(IndexAt([0, 0]).IsOuterCorner(bottomRight));
Insert(outer:bottomLeft).If(IndexAt([0, 0]).IsOuterCorner(bottomLeft));

// insert inner corners
Insert(inner:topLeft).If(IndexAt([0, 0]).IsInnerCorner(topLeft));
Insert(inner:topRight).If(IndexAt([0, 0]).IsInnerCorner(topRight));
Insert(inner:bottomRight).If(IndexAt([0, 0]).IsInnerCorner(bottomRight));
Insert(inner:bottomLeft).If(IndexAt([0, 0]).IsInnerCorner(bottomLeft));

And this pretty much wraps it up. We put our preset in a separate file that doesn't include base.r and it's ready for use. Now we can apply it to different tilesets:

// grass_main.r++
#include "base.r"
#include "border.p"

AutoMapper("Border");
preset.Border(1, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50, 51);

// winter_main.r++
#include "base.r"
#include "border.p"

AutoMapper("Border");
preset.Border(1, 19, 2.V, 98, 2, 16, 20, 100, 96, 4, 3, 7, 8);

Full code

preset Border(
    int fill,
    int wall:top, int wall:right, int wall:bottom, int wall:left,
    int outer:topLeft, int outer:topRight, int outer:bottomRight, int outer:bottomLeft,
    int inner:topLeft, int inner:topRight, int inner:bottomRight, int inner:bottomLeft
)

    if (s:debug)
        array int values = util:ArrayInt(
            fill, wall:top, wall:right, wall:bottom, wall:left,
            outer:topLeft, outer:topRight, outer:bottomRight, outer:bottomLeft,
            inner:topLeft, inner:topRight, inner:bottomRight, inner:bottomLeft
        );
        for (i = 0 to values.last)
            if (values[i] < 0 or values[i] > 255)
                error("preset.Border -> values must be in range [0-255].");
            end
        end
    end
    
    // fill everything
    Insert(fill);

    // insert walls
    Insert(wall:top).If(IndexAt([0, 0]).IsWall(top));
    Insert(wall:right).If(IndexAt([0, 0]).IsWall(right));
    Insert(wall:bottom).If(IndexAt([0, 0]).IsWall(bottom));
    Insert(wall:left).If(IndexAt([0, 0]).IsWall(left));

    // insert outer corners
    Insert(outer:topLeft).If(IndexAt([0, 0]).IsOuterCorner(topLeft));
    Insert(outer:topRight).If(IndexAt([0, 0]).IsOuterCorner(topRight));
    Insert(outer:bottomRight).If(IndexAt([0, 0]).IsOuterCorner(bottomRight));
    Insert(outer:bottomLeft).If(IndexAt([0, 0]).IsOuterCorner(bottomLeft));

    // insert inner corners
    Insert(inner:topLeft).If(IndexAt([0, 0]).IsInnerCorner(topLeft));
    Insert(inner:topRight).If(IndexAt([0, 0]).IsInnerCorner(topRight));
    Insert(inner:bottomRight).If(IndexAt([0, 0]).IsInnerCorner(bottomRight));
    Insert(inner:bottomLeft).If(IndexAt([0, 0]).IsInnerCorner(bottomLeft));
end
⚠️ **GitHub.com Fallback** ⚠️