Developing Add-Ons for CC3+ – Part 5: Dynamic Dungeon Tools 1

In this article in the development series, I’ll start putting the things we have learned into some proper useful commands for CC3+. I’ll be going for designing a set of dynamic dungeon tools that focuses on making the drawing of a dungeon quick and easy. In particular, I am aiming at making a set of tools that lets you draw the floorplan in a more fluid manner, and easily do things like changing the shape of a room by adding a small alcove or similar, without manually manipulating the entities. I am also making sure that the floor will always be merged to a single polygon so we avoid breaks in the fill pattern.

This will be a series of several articles, so in this first article we will be getting started with the basics. We will start by writing the code for drawing polygons, and we will see how we can merge them automatically to a larger polygon. This should give us a great starting point, which we will build upon in future articles. This short YouTube video shows a demo of what the code below achieves in CC3+.

To be able to follow this article series, you should have read my earlier articles in the series.

Step One – Drawing Polygons

So, let us start by drawing some polygons. Basically what we are doing here is that we first ask the user for all the nodes needed to draw the polygon, similar to the experience the user have when drawing a polygon normally. A right click will finish the collecting of nodes, and draw the polygon on screen.

As the user clicks points on screen, we will collect this to a temporary array, and once the user finishes (right click) we build up our entity using this list of collected nodes. The actual collection of nodes is handles by a single function, DDPolyGetNode(), which is called over and over as long as the user supplies more nodes.

If you run the code below, you will see that it behaves slightly different to a regular polygon drawing tool, as it only shows a rubberband between the previous node and the one you are placing, but not the whole outline of the polygon so far. We will address this in a future article, as the current behavior makes it a bit difficult to see the exact shape you are making, but for now, we’ll keep it like this.

To actually use the code below, also remember to register the DDPOLY command in the CList array, as well as a reference to DDPoly in PList, or it won’t actually be available to CC3+.

// All commands this XP provides should be listed here, separated by the NULL-character
char CList[] = "DDPOLY\0\0";

// This list contains the actual functions called by the CC3+ commands. It should always start
// with the About function, then followed by the functions the commands should call in the same
// order as the command list above.
PCMDPROC PList[] = { About, DDPoly };

The code below is explained via code comments. I’ve not included the .h file here as it doesn’t have any contents of particular interest. Download my entire project from the link at the bottom to see all my files. For the best learning experience, I do recommend you start up an empty CC3+ XP project and get my code below running without resorting to my full example.

// These three variables will be used to build up the polygon as the
// user click points. I've assumed 1024 points is enough here. Could
// have used a dynamic data structure like a vector instead, but this
// part will change in a future part anyway.
// Note that the 1024 limit here is for each shape added, not for the
// total composite floor we will end up with, so it is probably overkill.
GPOINT2 tempPoint;
GPOINT2 pointList[1024];
int nodecounter = 0;


// Define the prompt for the first node
FORMST(startNode, "Place start node: \0")

// Define the data request for the first node.
// Note that the data type we ask for is RD_2DC, which is a x,y coordinate pair. The user 
// can click these points int he drawing, or type them in on the command line
// The data returned from the request will be stored in the tempPoint variable and the 
// function DDPolyGetNode() will be called.
RDATA FirstPolyPointReq =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)&tempPoint,
(DWORD*)&startNode,RDC_XH, DDPolyGetNode, NULL, NULL, 0, NULL, 0 };


// This is the entry point for the DDPOLY command
// We reset the node counter back to zero each time we begin a new shape
// and then fire of the request for the first point
void XPCALL DDPoly() {

	nodecounter = 0;

	ReqData(&FirstPolyPointReq);

}



// Prompt and request used for all remaining nodes. The only real difference
// is the prompt text itself, and that instead of a crosshair cursor, we use a rubberband
// cursor to show a line from the previous point.
FORMST(nextNode, "Place next node (right click to finish): \0")
RDATA NextPolyPointReq =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)&tempPoint,
(DWORD*)&nextNode,RDC_RBAND, DDPolyGetNode, NULL, NULL, 0, NULL, 0 };


// This function handles the callback for each node the user creates
void XPCALL DDPolyGetNode(int Result, int Result2, int Result3) {

	// Right Click during a request is what CC3+ considers accepting the default value for
	// the prompt. In our case, this means we are done placing nodes, and wish to finish. 
	// So, we call the DDDrawPoly() function to handle the actual drawing
	if (Result == X_DFLT) { DDDrawPoly(); return; }

	// If the user supplied an invalid value, which includes hitting Esc to abort, we just 
	// abort the drawing and end the command.
	if (Result != X_OK) { CmdEnd(); return; }

	// Sets up the origin for the rubberband cursor
	NewCsrOrg(tempPoint.x, tempPoint.y);

	// Adds the last node supplied to our array of nodes
	pointList[nodecounter++] = tempPoint;

	// Fires of the data request. Since the request specifies this function, we will 
	// basically repeat this function as long as the user supplies nodes.
	ReqData(&NextPolyPointReq);

}


// This function creates the actual CC3+ entity for the polygon and draws it on screen
void DDDrawPoly() {

	// We need to know the length of the entity record to be able to allocate the correct 
	// amount of memory for this entity. The correct size is the size of the PATH2 structure
	// plus the size of the nodes, which is obviously the size of a single node (GPOINT2) 
	// multiplied by the number of nodes
	const auto len = static_cast<int>(sizeof(PATH2) + sizeof(GPOINT2) * nodecounter);
	
	// Here we create a vector of chars of the appropriate size to hold our entity. We're 
	// actually not interested in the vector itself, but the vector is great since it cleans
	// up the memory when it falls out of scope at the end of the function. We could instead
	// have reserved memory with malloc, but would also have needed to remember to free it 
	// too or we would end up with a memory leak
	std::vector<char> buffer(len);

	// Grab the data area from the vector and treat it like an entity record.
	auto p = reinterpret_cast<pENTREC>(buffer.data());

	// We set up the entity record with the basic settings. Here we just ensure that all the 
	// variables are set to 0 since we don't know what was in the memory area we grabbed. We 
	// also set the entity record length which is important for CC3+ to know when it does 
	// it's own memory allocation and the type of entity. 
	// The entity type here is ET_PATH2 which is used for both paths and polygons, since 
	// polygons are just closed paths.
	p->CStuff = { len, ET_PATH2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0 };

	// We set up our path to be closed. This weird double access to "Path" happens because 
	// we are working with an entity record and not a path type directly.
	p->Path.Path.Flags = NL_CLS;
	
	// SParm and EParm are used to trim entities. To ensure the entity is not trimmed, SParm 
	// should be 0 (start of entity) and EParm be equal to the number of nodes (for 
	// polygons, paths should have number of nodes minus one. Polys need that one extra 
	// because of the closing line between the last node and the first)
	p->Path.Path.SParm = 0;
	p->Path.Path.EParm = (float)nodecounter;
	

	// Need to tell CC3+ how many nodes we have
	p->Path.Path.Count = nodecounter;

	// This loop simply fetches all the nodes from our temporary storage and puts them into 
	// the entity node list
	for (; nodecounter > 0; nodecounter--) {
		p->Path.Path.Nodes[nodecounter - 1] = pointList[nodecounter - 1];
	}

	// Get the current settings in CC3+ (such as layer, fill style, line style, etc) and 
	// populates the entity with them
	GetCStuff(p);

	// We are adding something to our drawing list which the user may wish to undo. Placing
	// an undo marker here. MarkUndoAdd() are used when setting up undo point for things
	// not requiring a selection, such as when adding new entities to the drawing.
	MarkUndoAdd();

	// Insert the entity into the main drawing list.
	pENTREC ent = DLApnd(NULL, p);

	// Draw our new entity out to screen
	EDraw(ent);

	// Telling CC3+ that our command is done. This is important.
	CmdEnd();
	
}

I recommend you try out this code in CC3+. You should be able to draw polygons using your new command. Note that we grabbed the current settings when making our polygon, so it will be drawn on the current sheet/layer with current options for line and fill. Change these before using the command. This behavior is exactly the same as you see from any basic tool in CC3+.

Step Two – Merging Polygons

Now, the main idea of this project was to create tools that behave a bit more dynamically. I want to draw one polygon, and then I want to be able to basically expand that polygon by drawing another polygon partly on top of it. If we do that with normal polygons in CC3+, it is pretty obvious that we have just slapped two polygons partially on top of each other, because they won’t merge seamlessly unless you happened to be really lucky with the placement in relation to the fill tiling. You can see a typical result in the top image to the right. You can clearly see that the tiling don’t match up between the two polygons.

For our result, I want something more like the bottom right image, where you can see the fill is seamless. To be able to do this, we need to merge our two polygons into one polygon. This is a process we can do inside CC3+, but it do end up quite a bit of work for a larger dungeon, so my goal here is to do it automatically as we draw.

So, I started out writing code that could merge two polygons. This code is actually independent of the code above, because I wanted to be able to reuse it to merge any two polygons.

This code is still just considered a rough first version though, as it has multiple limitations, so it is by no means a finished product. One of the current limitations is that it only looks at the FLOORS sheet. This makes sense for a dungeon, but our code above can draw on any sheet. Since this is going to be dungeon tools, sticking to the floor sheet makes sense however, so I’ll leave it at that, but keep that in mind when we combine the two tools, that if FLOORS isn’t the current sheet, the entities we draw with our above command won’t end up on the right sheet, and nothing will be merged.

Another limitation is that I just grab the two first polygons I find on the FLOORS sheet. The idea here is that the entire floor will be a single polygon, so there will never be more than two polys on the floor, the current floor, and the new one I drew that will be merged into it. This is all well and good when starting out with a new blank map, but if you try to use it on an existing map in this state, I can promise it won’t work as intended. This is one of the points we will be working on in future installments of this series.

One of the more interesting points of the code below is how I merged the two polygons into one. This is actually a well-understood mathematical problem, but instead of implementing the algorithm myself, I learned that the Boost libraries for C++ contains this implementation. Boost is pretty popular, but I haven’t used it myself before, so I relied on a couple of tutorials to figure out how to read in my coordinates from CC3+. The example used the Well-known text representation (WKT) for geometry, so this had me convert the coordinates to text strings. There may be another option for doing this, but creating some text strings worked well enough for me. To install the Boost library into my solution, I just right clicked my project in Visual studio and picked Manage NuGet Packages. I then searched for Boost and installed the main Boost package (the one named just boost, and not one of the myriad others). The headers I needed to include are shown in the code below.

I haven’t yet tackled the issue of the polygons NOT overlapping either. So for now, this requires two overlapping polys to work. If you draw two disjunct polys, it will simply result in the first one staying as it is and the second one being erased. If there are more than two, it will pick the two first ones.

As I mentioned above, the code below is stand alone. So if you register the DDMerge function in CList and PList you can call it directly. You can then test this by having two polygons on you FLOORS sheet and run it. Make sure there are only two, and that they partially overlap. You can draw these polys using the DDPOLY command from above, or manually using other tools.

// Required includes for boost
#include <boost/geometry/geometry.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/geometries/polygon.hpp>
#include <boost/geometry/io/wkt/wkt.hpp>


// Main entry point for the DDMERGE command. Don't forget to update CList and PList
void XPCALL DDMerge() {

	// Call a custom functions that looks up a sheet by name and return the entity record
	// for that sheet. If we don't get a valid entity record back, we immediately terminate
	pENTREC sheet = FindSheet("FLOORS");
	if (!sheet) { CmdEnd(); return; }

	// To find entities on a sheet, we need the sublist for the sheet. This command
	// retrieves it, and if it doesn't have a sublist, we terminate
	hDLIST hSubL = DLGetSubList(sheet);
	if (hSubL == NULL) { CmdEnd(); return; }

	// We scan the sublist belonging to the sheet looking for the fist polygon we can find
	// The job of actually checking for polygon is handled by the callback function
	// DDMergeFindEntity. If we don't get a valid entity record back, we terminate
	// We also cast the DWORD, which is a memory address, to a pointer to an entity record
	DWORD firstEntity = DLScan(hSubL, DDMergeFindEntity, DLS_Std, NULL, NULL);
	if(firstEntity == 0) { CmdEnd(); return; }
	pENTREC fe = (pENTREC)firstEntity;

	// Now looking for the second polygon on the sheet. Only difference from above is that we
	// send the first entity record as a parameter so this scan can skip that one so we don't
	// get the same twice.
	DWORD secondEntity = 
			DLScan(hSubL, DDMergeFindEntity, DLS_Std, (pENTREC)firstEntity, NULL);
	if (secondEntity == 0) { CmdEnd(); return; }
	pENTREC se = (pENTREC)secondEntity;

	// Casting the entity records returned earlier to PATH2 entities. If any of them turns
	// out to not be a valid PATH2 entity, we terminate.
	const auto poly1 = XP_PATH2_CAST(fe);
	const auto poly2 = XP_PATH2_CAST(se);
	if(!(poly1 && poly2)) { CmdEnd(); return; }


	// defines a neater type name for the boost type we need
	typedef 
	 boost::geometry::model::polygon<boost::geometry::model::d2::point_xy<double> > polygon;

	// declares two boost polygons
	polygon p1, p2;

	// Fills in the coordinates from CC3+ into the boost polygons.
	// We are using a helper method, BuildWKTString(), to convert the Path stored
	// in the entity record into a properly formatted text string
	boost::geometry::read_wkt(
		BuildWKTString(&(poly1->Path))
		, p1);

	boost::geometry::read_wkt(
		BuildWKTString(&(poly2->Path))
		, p2);

	// These lines just ensures the data is properly normalized for boost (order of nodes)
	boost::geometry::correct(p1);
	boost::geometry::correct(p2);
	if (boost::geometry::area(p1) < 0) boost::geometry::reverse(p1);
	if (boost::geometry::area(p2) < 0) boost::geometry::reverse(p2);

	// Define a vector to store the combined polygon
	std::vector<polygon> combined;

	// Have boost combine our two polygons and place the result in our vector. We're only
	// going to get one result back if the polygons overlap.
	// We are currently not handling the situation where they don't overlap, resulting
	// in the new polygon simply being erased if it don't overlap the old one.
	boost::geometry::union_(p1, p2, combined);

	// We're going to be changing entities now. Setting up an undo point for the user
	MarkUndoAdd();

	// This creates a clone of our existing entity for editing. The original entity is
	// marked as deleted so it is removed from the drawing, but still retriavable via undo.
	pENTREC editEntity = DLClone(fe);

	// Figuring out the number of nodes in our combined polygon
	const int numNodes = boost::geometry::num_points(combined[0]);

	// Calculates new entity record length based on the new node count
	const auto len = static_cast<int>(sizeof(PATH2) + sizeof(GPOINT2) * numNodes);
	
	// Resize the entity to the new length. We MUST do this before adding the new nodes
	// or we would start writing into memory occupied by other entities, corrupting the
	// drawing
	editEntity = DLResize(editEntity, len);

	// For simplicity, lets us handle it as a PATH2 entity
	const auto editPoly = XP_PATH2_CAST(editEntity);

	// Sets the number of nodes in the entity to the new number
	editPoly->Path.Count = numNodes;

	// And also updates EParm as well so we can see the new nodes added
	editPoly->Path.EParm = (float)numNodes;

	// Grabs the nodes from our boost poly and feeds them into the CC3+ entity.
	// Note that we overwrite the entire node list, since some of the original nodes may be
	// gone. Trying to figure out which nodes to insert is useless and error-prone work.
	for (int i = 0; i < numNodes; i++) {
		editPoly->Path.Nodes[i].x = combined[0].outer()[i].x();
		editPoly->Path.Nodes[i].y = combined[0].outer()[i].y();
	}


	// We erase the second entity from the drawing. Since the original entity have been
	// updated to be a combined entity, the newly drawn entity shouldn't hang around anymore
	DLErase(se);

	// Update the screen with the edited entity.
	EDraw(editEntity);

	// Properly terminate the command
	CmdEnd();

}


// This function is used when we scan the drawing list for polygons. It simply return the
// first polygon it finds, unless it matches the one sent in as parm1, is so it skips
// over that one and looks for the next.
// Note that this functions is called once for every entity in the drawing list, it's job
// is simply to check if the entity it gets fits our criteria. If it does, we return
// the entity, if not, we return 0 which tells DLScan to continue processing the 
// Drawing Lsit and call this function again with the next entity, if any.
pENTREC XPCALL DDMergeFindEntity(hDLIST list, pENTREC entity, const DWORD parm1,
	                                                                          DWORD parm2) {
	
	// Cast parm1 to a pointer to an entity record and compares it with the current one
	// If they match, this means we don't want it, since that's the one returned from
	// the first scan.
	pENTREC another = (pENTREC)parm1;
	if (entity == another) return 0;

	// Cast the entity to a PATH2. This cast only succeeds if the entity represents a 
	// PATH2 type entity. Remember that PATH2 covers both paths and polygons
	// Right now, we don't actually do any more checking to figure out if it is really
	// a polygon and not a path, perhaps we should?
	const auto poly = XP_PATH2_CAST(entity);

	// If the call above succeeded, we return the entity.
	if (poly) {
		return entity;
	}

	// This is not the the entity you are looking for. Move along.
	return 0;
}




// Helper function for finding a sheet by name
pENTREC FindSheet(std::string sheetName) {

	// We scan the drawing list with DLS_SHEET flag set, so we see the sheets instead of the
	// entities they contain. The name we are looking for are sent as a parameter for use in
	// the callback function
	DWORD result = DLScan(NULL, FindSheetCallback, DLS_SHEET, &sheetName, NULL);

	// Return the entity record for the sheet. If the sheet wasn't found the return value 
	// will be 0. This needs to be handled by the caller.
	return (pENTREC)result;


}

// Callback for our sheet scan
pENTREC XPCALL FindSheetCallback(void* hDList, const pENTREC pEntRec, const DWORD parm1,
	                                                                          DWORD parm2) {

	// Tries to cast any received entity to a SHEET type entity. Will fail if the entity
	// isn't a sheet
	const auto sheet = XP_SHEET_CAST(pEntRec);

	// If we succeeded above, we can now check the sheet name. If it matches we return the
	// entity record for the sheet.
	if (sheet) {

		std::string* sname = (std::string*)parm1;

		if (strcmp(sheet->SName, sname->c_str()) == 0) {
			return pEntRec;

		}

	}

	// Nothing to see here. Move along.
	return 0;

}


// Helper function to build a WKT string for input to the boost geometry functions
// A WKT string looks like this:
// POLYGON(( X1 Y1, X2 Y2, X3 Y3, ...))
// (They can be far more complex, but this is what we need now.)
std::string BuildWKTString(GPATH2 * path) {

	std::string buff = "POLYGON((";
	for (int i = 0; i < path->Count; i++) {
	    buff.append(
	        std::to_string(path->Nodes[i].x) + " " + std::to_string(path->Nodes[i].y) + ", "
	    );
	}
	buff.append("))");

	return buff;

}

Step Three – Tying It Together

With the above code, we now have a function that let us draw polygons, and another that merges two polygons into one. As it stands right now, the first one is kind of useless, because all it does is to be a poor replacement for the standard polygon tool. But my plan all along was to tie them together so that whenever you draw something, everything is automatically merged.

So, let us tie everything together, and all that’s required to do that is to replace the call to CmdEnd() at the end of DDDrawPoly() with DDMerge(). So basically, instead of ending the command after drawing a poly, we hand things over to our merge command instead. The net result of this is that every time I draw a poly using DDPoly, it will be merged with the existing poly on the sheet.

We still have the limitation that there shouldn’t be other polys on the sheet, and they need to overlap, otherwise the new poly we draw will be immediately deleted. However, it is not a problem drawing our first, lone poly, because DDMerge() will automatically terminate when it can’t find two polys. So, try it out for yourself. You can see the YouTube video linked at the top of the article for a short demo of how it should work. Just notice how those polygons instantly merge as you put them down.

Further Steps

As already mentioned, this version is still pretty rough. It handles the main functionality, but it has heaps of limitations, for example what happens if there are entities on the sheet already. And we’re still not forcing the entities from the DDPoly commad to the right sheet, so if you haven’t set floors as your sheet before running the command, you can draw polys, but they won’t merge.

And of course, the floor is just the start. While this isn’t designed as a do-everything command, people will still have to populate their dungeon, we should at least introduce walls around our dungeon that updates automatically when you add a section. We also need the polygon tool to be a bit prettier when we draw with it. And so far we can only add to our dungeon, we can’t erase anything, nor make holes in the floor. So, still lots to do and explore.

Download

You can download my solution. Note that it was written using Visual Studio 2022. Not sure if the solution file will work in earlier versions, but you should still be able to use the source code files just fine. I compile the project itself using the 1.42 version of the platform toolset (from Visual Studio 2019), this is the same version CC3+ is compiled under, and should ensure no extra runtimes required for this XP to work.

 

If you have questions regarding the content of this article, please use the ProFantasy forums. It can take a long time before comments on the blog gets noticed, especially for older articles. The forums on the other hand, I frequent daily.

 

 

Comments are closed.