• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Zinc]TerrainTile & TerrainSwap

Introduction:

I decided to make these scripts because I was making a spell that changed the terrain temporarily, and I ran into all kinds of problems. How to store the old terrain that was there was the problem: I fixed that by making a simple array of values holding the terrain type and variation of the original. It worked great until I cast another spell overlapping the first -- by the time both spells had expired, the terrain stayed changed where they overlapped. I tried to build something into the spell, but it was futile -- it was too complicated to incorporate into a spell script. I needed something else. That's why I wrote these libraries.

So what do they do?

TerrainTile can change the terrain at any given point, and no matter how many are created over each other, when they are all removed, the original tile will still be there.

TerrainSwap is a holder for multiple TerrainTiles. It allows you to create multiple TerrainTiles over any area, by a selection of several methods, and apply or remove them all at once.

Here is the code:

Requires LinkedList by Ammorth

[jass=TerrainTile]/***************************************************************************
* TerrainTile by Element of Water
****************************************************************************
* What is it?
* TerrainTile provides a way to swap any single terrain tile on the map
* with a different one, just like SetTerrainType. The difference is, with
* this system, when the TerrainTile is destroyed, the tile that was
* originally there, that the TerrainTile overwrote, will reappear. You
* can also stack multiple TerrainTiles on top of one another, and
* destroying the top one will reveal the next, etc, in the same order
* they were applied in. Don't worry about destroying tiles that weren't
* on top, either -- they will be removed from the list.
****************************************************************************
* How do I use it?
* Using TerrainTile couldn't be easier -- there are just a few functions
* to call to control it.
* 1) Create your TerrainTile:
* set MyTerrainTile = TerrainTile.create(real x, real y, integer terrainType, integer variation)
* real x/y -- the coordinates of your TerrainTile
* integer terrainType -- the terrain tile's type id, this can be found
* by converting an Environment - Change Terrain
* Type GUI action into JASS and copying the value
* in 'AAAA' that you see there
* integer variation -- this is just the same as the "variation" when
* placing tiles in the terrain editor, except
* you can have a variation of -1, which means
* "random variation"
* 2) Apply (show) your TerrainTile:
* call MyTerrainTile.apply()
* 3) Change its properties:
* set MyTerrainTile.terrainType = NewTerrainType
* set MyTerrainTile.variation = NewVariation
* 4) Reapply it, or change its location:
* call MyTerrainTile.setCoords(newX, newY)
* 5) When you're done with it, but you'll use it again, remove it:
* call MyTerrainTile.remove()
* 6) Or if you're finished with it for good, destroy it:
* call MyTerrainTile.destroy()
*
* There is also a function called TerrainTile.coord2TileCoords, which
* takes any single coordinate x, and returns its closest multiple of 128,
* the size of a tile. I figured I'd make it public because some people
* might want to use it.
***************************************************************************/
//! zinc
library TerrainTile
{
private keyword next;
private keyword prev;

private struct TerrainList
{
TerrainTile first = 0;

method addLink (TerrainTile link)
{
link.next = first;
first.prev = link;
first = link;
}

method removeLink (TerrainTile link)
{
link.next.prev = link.prev;
link.prev.next = link.next;
if (link == first) first = link.next;

link.next = 0;
link.prev = 0;
}
}

public struct TerrainTile
{
// shouldn't be changed, unless blizz changes it in a patch,
// which is highly unlikely
static constant real TILE_SIZE = 128.;
// the hashtable to store the tiles in
private static hashtable hash = InitHashtable();

// internal variable used to separate default tiles,
// the ones which are there at the start of the map,
// from those which are changed with this system
private boolean isDefault = false;
// internal variables basically there to save processing
// power
private integer ix = 0, iy = 0;
// the coordinates of the tile -- access them with "x"/"y",
// not "xx"/"xy" -- don't worry if these are
// different to the ones you put into the create function
// as they are rounded to the nearest multiple of 128
// can't be altered directly, as that could screw up
// the whole system
private real xx = 0., xy = 0.;
// the rawcode of the terrain type of the tile -- you can
// alter this directly but you won't see anything unless
// you call apply() again
integer terrainType = 0;
// the variation of the tile -- again, you can alter this
// but it won't be visible until apply() is called
// note: a variation of -1 means random, and this means
// it might change in appearance when another tile is
// created over it and then removed, or even if you call
// apply() again.
integer variation = 0;
// you can store data on the TerrainTile if you want to...
integer data = 0;

// linked list data, used to store which tiles are at the
// same place as this one
TerrainTile prev = 0;
TerrainTile next = 0;
private TerrainList list = 0;

// this method changes the position of the tile
method setCoords (real newX, real newY)
{
// if the tile has been applied already, flag it for
// reappliance
boolean reapply = list != 0;
// if it needs to be reapplied, temporarily remove the
// tile
if (reapply) remove();
// convert the new coordinates into usable ones, store them
xx = coord2TileCoord(newX);
xy = coord2TileCoord(newY);
// also store them as integers as multiples of 128, for
// optimization reasons
ix = R2I(xx / TILE_SIZE);
iy = R2I(xy / TILE_SIZE);
// if it needs to be reapplied, apply it again
if (reapply) apply();
}

// these operators return the values of the readonly variables x and y
method operator x () -> real {return xx;}
method operator y () -> real {return xy;}

// this method adds the tile to the top of the list and displays it
method apply ()
{
TerrainTile t;
// I need to comment functions like these to keep my sanity...

// if the tile is not already linked, link it
if (list == 0)
{
// load the list for this xx/xy position from the hashtable
list = TerrainTile(LoadInteger(hash, ix, iy));
// if the list doesn't exist...
if (list == 0)
{
// create one...
list = TerrainList.create();
// ...and save it in the hashtable
SaveInteger(hash, ix, iy, integer(list));

// also, this means the tile has not been changed, so we need
// to store the tile that was originally there...
// allocate the new tile struct...
t = TerrainTile.allocate();
// store the coordinates
t.ix = ix; t.iy = iy;
t.xx = xx; t.xy = xy;
// store the terrain type
t.terrainType = GetTerrainType(xx, xy);
// store the terrain variation (why does blizz call it "Variance"? o_O
t.variation = GetTerrainVariance(xx, xy);
// this is the default terrain, make sure the system knows so
t.isDefault = true;
// create a link for the new tile struct in the new list
list.addLink(t);
}
// create the link in the list
list.addLink(this);
}
// else, bring the link to the front of the list
// if it isn't there already
else if (this != list.first)
{
// destroy the link, ready to recreate it...
list.removeLink(this);
// recreate it at the front of the list
list.addLink(this);
}

// finally, change the actual visible terrain!
SetTerrainType(xx, xy, terrainType, variation, 1, 0);
}

// this method removes the tile from the list, and replaces
// it with the next highest if it is at the top
method remove ()
{
TerrainTile t;

// again, this definitely needs commenting...

// if the tile isn't linked, we don't need to remove it --
// it doesn't actually exist

// otherwise, yeah, remove it...
if (list != 0)
{
// first thing's first, destroy the link
list.removeLink(this);
// now if the list contains other data, apply that terrain type
if (list.first != 0)
{
// store the previous tile in a variable so it can be accessed easily
t = list.first;
// apply the previous terrain type
SetTerrainType(xx, xy, t.terrainType, t.variation, 1, 0);
// if the previous tile was the default one, we can safely destroy it
if (t.isDefault)
{
// destroy the link to the default tile
list.removeLink(t);
// null the reference so the destructor doesn't screw up
t.list = 0;
// destroy the default tile
t.destroy();
// now the list should be empty, we can destroy that, too
list.destroy();
// make sure the hashtable knows the list is gone
RemoveSavedInteger(hash, ix, iy);
}
}
// if the list doesn't contain other data, it is empty, destroy it
// this shouldn't happen since there should always be a default tile
// at the bottom, but it's just a precaution
else
{
// destroy the list
list.destroy();
// make sure the hashtable knows the list is gone
RemoveSavedInteger(hash, ix, iy);
}
// now it is safe to null the reference to the list
list = 0;
}
}

// this could be a useful function I guess, so I'll leave it
// public...
// basically, it rounds xx to the nearest multiple of 128
static method coord2TileCoord (real x) -> real
{
// get the modulus of the coordinate divided by 128
real mod = x - I2R(R2I(x / TILE_SIZE)) * TILE_SIZE;
// make sure the modulus is in the range 0...128
if (mod < 0.) mod += TILE_SIZE;

// round it down to start with
x -= mod;
// and if it was at the upper end of the multiple
// of 128, bring it back up by 128
if (mod > TILE_SIZE / 2.) x += TILE_SIZE;

// return the rounded value
return x;
}

static method create (real x, real y, integer terrainType, integer variation) -> TerrainTile
{
// allocate a new TerrainTile instance
TerrainTile t = TerrainTile.allocate();
// store useable coordinates (multiples of 128)
// for real-space operations
t.xx = coord2TileCoord(x);
t.xy = coord2TileCoord(y);
// store shortened integer versions of the
// coordinates for hashtable operations
t.ix = R2I(t.xx / TILE_SIZE);
t.iy = R2I(t.xy / TILE_SIZE);
// store the terrain type id and variation
// for later access
t.terrainType = terrainType;
t.variation = variation;
// return the new struct
return t;
}

method onDestroy ()
{
// make sure the TerrainTile is completely erased
remove();
// to let users check if the TerrainTile still
// exists and is associated with their data
data = 0;
}
}
}
//! endzinc

[/code]
[jass=TerrainSwap]/***************************************************************************
* TerrainSwap by Element of Water
****************************************************************************
* Requirements:
* TerrainTile by Element of Water
* LinkedList by Ammorth
****************************************************************************
* What is it?
* A TerrainSwap object is simply a container for several TerrainTiles.
* It provides ways of creating several at once, arranged it circles,
* squares, or contained within a certain rect, and it can also apply
* and remove them all at once.
****************************************************************************
* How do I use it?
* The principle is basically the same as TerrainTile -- you create it,
* do things to it, apply it, then remove or destroy it.
* 1) Create your TerrainTile:
* set MyTerrainSwap = TerrainSwap.create(integer terrainType, integer variation)
* integer terrainType -- the terrain tile's type id, this can be found
* by converting an Environment - Change Terrain
* Type GUI action into JASS and copying the value
* in 'AAAA' that you see there
* integer variation -- this is just the same as the "variation" when
* placing tiles in the terrain editor, except
* you can have a variation of -1, which means
* "random variation"
* 4) Add some tiles to your TerrainSwap:
* This adds a single tile using the type and variation of the whole
* TerrainSwap, at the specified x/y coordinates.
* call MyTerrainSwap.addTile(real x, real y)
* This adds a circle just like SetTerrainType with shape 0, at
* coordinates x/y with size area.
* call MyTerrainSwap.addCircle(real x, real y, integer area)
* This adds a square just like SetTerrainType with shape 1, at
* coordinates x/y with size area.
* call MyTerrainSwap.addSquare(real x, real y, integer area)
* This adds a circle or a square just like SetTerrainType, at
* coordinates x/y with size area. TERRAIN_SHAPE_CIRCLE is for a circle
* and TERRAIN_SHAPE_SQUARE is for a square.
* call MyTerrainSwap.addShape(real x, real y, integer area, integer shape)
* This one takes a rect and fills it with tiles:
* call MyTerrainSwap.addRect(rect r)
* 3) Apply (show) your TerrainSwap:
* call MyTerrainSwap.apply()
* 4) When you're done with it, but you'll use it again, remove it:
* call MyTerrainSwap.remove()
* 5) Or if you're finished with it for good, destroy it:
* call MyTerrainSwap.destroy()
***************************************************************************/
//! zinc
library TerrainSwap requires TerrainTile, LinkedList
{
public struct TerrainSwap
{
// these are for use in addShape() -- I don't think they
// need any explaining :p
static constant integer SHAPE_CIRCLE = 0;
static constant integer SHAPE_SQUARE = 1;

// the list used to store all the TerrainTiles
private List list = 0;
// stored values of terrain type rawcode and variation --
// I might make these public and add the necessary
// functionality one day, but not now
private integer terrainType = 0, variation = 0;

// a rather simple method that adds a tile to the
// TerrainSwap, ready to be swapped
method addTile (real x, real y)
{
// create a link in the list containing data from a
// newly created TerrainTile
Link.create(list, integer(TerrainTile.create(x, y, terrainType, variation)));
}

// a more complicated method to add several tiles in
// the shape of a circle to the TerrainSwap, with
// the integer area being the diameter of the circle in
// number of tiles, just like the SetTerrainType
// native with the shape parameter set to 0
private real xp; // these variables are used
private real yp; // to pass values between addCircle
private real rad; // and addCircle_child (see below)
private real radsq;
private real xx, yy;
method addCircle (real x, real y, integer area)
{
// calculate the distance the method needs to
// check to draw the tiles
real dist = (area - 1) * TerrainTile.TILE_SIZE;
// the starting x coordinate is simply the origin
// minus the distance
xp = - dist;
// the radius is simply the distance to the starting
// x/y positions, but it needs to have a small value
// added to make sure the checks encompass all points
rad = dist + 1.;
// square the radius for efficiency in distance checking
radsq = rad * rad;
// make sure the child method has access to the x
// and y origins of the circle
xx = TerrainTile.coord2TileCoord(x);
yy = TerrainTile.coord2TileCoord(y);

while (xp < rad)
{
// tells the child method where to start its
// y loop from
yp = - dist;
// the y loop needs to be run in a different
// thread to avoid hitting the op limit
addCircle_child.execute();

// increment the x value
xp += TerrainTile.TILE_SIZE;
}
}

// child method of addCircle runs the y loop in a
// separate thread to avoid hitting the op limit
private method addCircle_child ()
{
// we need to make sure the method doesn't hit
// the op limit, and that is what this variable is
// for
integer i = 5;
// loop through y as long as we're within the radius
while (yp < rad)
{
// if i reaches 0..
if (i == 0)
{
// restart the thread
addCircle_child.execute();
// and break the current loop
break;
}
// use Pythagoras' theorem to determine whether
// the point is within the circle
else if (xp * xp + yp * yp <= radsq)
// if the point is within the circle, add
// a tile there
addTile(xx + xp, yy + yp);
// decrement the op checker
i -= 1;
// increment the y value
yp += TerrainTile.TILE_SIZE;
}
}

// adds several tiles in the shape of a square to the
// TerrainSwap -- works exactly like SetTerrainType
// with shape 1
method addSquare (real x, real y, integer area)
{
// calculate the distance the method needs to
// check to draw the tiles
real dist = (area - 1) * TerrainTile.TILE_SIZE;
// I'm using the same variables as for the circle
// method, even if they are used for different
// things -- in this case, the radius is simply
// half the width of the square (plus the small
// value)
rad = dist + 1.;
// the starting x coordinate is simply the origin
// minus the distance
xp = - dist;
// make sure the child method has access to the x
// and y origins of the square
xx = TerrainTile.coord2TileCoord(x);
yy = TerrainTile.coord2TileCoord(y);

while (xp < rad)
{
// tells the child method where to start its
// y loop from
yp = - dist;
// the y loop needs to be run in a different
// thread to avoid hitting the op limit
addSquare_child.execute();

// increment the x value
xp += TerrainTile.TILE_SIZE;
}
}

// child method of addSquare runs the y loop in a
// separate thread to avoid hitting the op limit
private method addSquare_child ()
{
// we need to make sure the method doesn't hit
// the op limit, and that is what this variable is
// for
integer i = 5;
// loop through y as long as we're within the radius
while (yp < rad)
{
// if i reaches 0..
if (i == 0)
{
// restart the thread
addSquare_child.execute();
// and break the current loop
break;
}
// no need for checks if the point is in the
// circle now, it's a SQUARE! :p
else addTile(xx + xp, yy + yp);
// decrement the op checker
i -= 1;
// increment the y value
yp += TerrainTile.TILE_SIZE;
}
}

// for those who like the format of SetTerrainType, this
// adds a circle or a square to the TerrainSwap
// the parameters mean exactly the same as their equivelent
// parameters in SetTerrainType.
method addShape (real x, real y, integer area, integer shape)
{
// if the shape is a circle, add a circle
if (shape == SHAPE_CIRCLE) addCircle(x, y, area);
// or if the shape is a square, add a square
else if (shape == SHAPE_SQUARE) addSquare(x, y, area);
// unrecognised shape -- give an error message
else debug BJDebugMsg("TerrainSwap.addShape: ERROR -- invalid shape specified (" + I2S(shape) + ").");
}

// a method to swap the terrain of all tiles in a rect
private real minx, maxx;
private real miny, maxy;
method addRect (rect r)
{
// store the rect as a set of coordinates
// temporarily
minx = TerrainTile.coord2TileCoord(GetRectMinX(r));
miny = TerrainTile.coord2TileCoord(GetRectMinY(r));
maxx = TerrainTile.coord2TileCoord(GetRectMaxX(r));
maxy = TerrainTile.coord2TileCoord(GetRectMaxY(r));
// store where to start off across the x plane
xp = minx;
// loop through x as long as we're within the rect
while (xp < maxx)
{
// tell the child method where to start the y
// from
yp = miny;
// run the y loop in a separate thread to avoid
// hitting the op limit
addRect_child.execute();
// increment the x value
xp += TerrainTile.TILE_SIZE;
}
}
// child method of addRect runs the y loop in a
// separate thread to avoid hitting the op limit
private method addRect_child ()
{
// we need to make sure the method doesn't hit
// the op limit, and that is what this variable is
// for
integer i = 5;
// loop through y as long as we're within the radius
while (yp < maxy)
{
// if i reaches 0..
if (i == 0)
{
// restart the thread
addRect_child.execute();
// and break the current loop
break;
}
// add the tile
else addTile(xp, yp);
// decrement the op checker
i -= 1;
// increment the y value
yp += TerrainTile.TILE_SIZE;
}
}

method apply ()
{
// get the first link in the list ready to operate on
Link l = list.first;
// loop through the entire list
while (l != 0)
{
// apply the TerrainTile associated with the
// current link
TerrainTile(l.data).apply();
// move on to the next link
l = l.next;
}
}

method remove ()
{
// get the first link in the list ready to operate on
Link l = list.first;
// loop through the entire list
while (l != 0)
{
// remove the TerrainTile associated with the
// current link
TerrainTile(l.data).remove();
// move on to the next link
l = l.next;
}
}

static method create (integer terrainType, integer variation) -> TerrainSwap
{
TerrainSwap t = TerrainSwap.allocate();

t.list = List.create();
t.terrainType = terrainType;
t.variation = variation;

return t;
}

method onDestroy ()
{
// iterate through all the TerrainTiles and destroy them...
// get the first link in the list ready to operate on
Link l = list.first;
// loop through the entire list
while (l != 0)
{
// remove the TerrainTile associated with the
// current link
TerrainTile(l.data).destroy();
// move on to the next link
l = l.next;
}

list.destroy();
}
}
}
//! endzinc[/code]
 
Last edited:
Level 8
Joined
Oct 3, 2008
Messages
367
Hmm.

It doesn't take much effort to implement your own linked list. It would cut off a rather unnecessary requirement.

I'm certain you're capable of doing so, and it would be a welcome improvement to your system. One that will most likely be repaid with approval, I might add.
 
A list works, and is no less efficient (like efficiency matters here anyway?). I think I'll stick with a list.

EDIT:
Actually, having looked at Anitarf's Stack, I have decided that stacks would indeed be simpler to use. I have changed TerrainTile to be library-independant (see first post (I was forced to add my own small List struct)), but doing the same for TerrainSwap would mean I have to practically recreate a list or stack system inside my own library, which defeats the object of the whole concept of libraries and modularisation. I'm going to make both libraries use Stack, unless you (Azlier) have anything to say against that.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Good : D

But do you really need size from Anitarf's? The problem with the wc3c libs is they always seem to be cumbersome.. they include extraneous features (maximizing coupling and minimizing cohesion).

Also stacks are extremely simple to create.... since your head is only stored in one area, it'd just be

JASS:
tile = Tile.create()
set tile.next = head
set head = tile

I think coding in a specific way for your specific library would be the best option here : ). There are a few different ways to code stacks and just using the generic way isn't always the right way to go : P.

And given you don't need complex composite objects, i won't link Stacked Fields to you >: P.
 
Argh, run into a problem using stacks: I need to be able to remove a TerrainTile from this "stack" even if it isn't at the top, as they can be removed in any order. Should I stick with Stack, adding a flag to each TerrainTile which means it is removed as soon the one above it is popped (which would require a little O(n) loop), or revert to LinkedList?

@Nestharus
Efficiency doesn't matter too much here, and I'd like to keep it approve-able at wc3c too.
 
Level 8
Joined
Oct 3, 2008
Messages
367
You know, perhaps you could merge the systems. That would allow the most functionality in one script instead of splitting it between two rather similar ones. TerrainTile standalone isn't too terribly much of an improvement over other terrain tiling systems. TerrainSwap, however, can be.
 
Top