Developing Add-Ons for CC3+ – Part 6: Dynamic Dungeon Tools 2

Last time in the developer series I started our Dynamic Dungeons project with the intention to showcase how to make some simple tools for a more fluid dungeon editing experience. In this issue, I will continue on with that project, and add some improvements to it, such as taking care that our entities are placed on the right sheets, meaning we will need to dive into sublists, and I will also automatically generate walls to go along with our floors.

As last time, I prepared a short video to show the tools in action. At the end of the video, you’ll also see that I show the classic dungeon tools correctly interacting with my entities.

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

So, when we finished up last time, we had a working solution that allowed us to draw the floor by drawing multiple individual partly overlapping polygons, and our code automatically joined these into a single polygon. Our solution had some limitation, like the inability to make a structure that had a hole in the middle, and it was overall just a proof of concept. Today, we’ll be working on adding some more functionality, and working on making everything a little more well-behaved. When we finish up today, we will still have some of the limitations, but we should be a bit further along the desired path. So, let us just dive into this.

I assume you are familiar with my code from the previous installment, so I won’t repeat everything here. Since we are editing existing code, this means it will be a bit more code snippets this time, as opposed to full functions. You’ll find the download link for my complete code at the end of the article.

Sheets and Layers

An obvious criteria for well-behaved entities is that they go to the right sheet and layer. By default, the DD3 dungeon tools use a set of predefined sheets/layers, so I will use the same ones for my project here. This means the floors needs to go on the FLOORS sheet and BACKGROUND (FLOOR 1) layer, while the walls go to the WALLS sheet and layer.

Last time, I did make a utility function to find the entity for any sheet by name. I used this because we only scanned the FLOORS sheet for entities to merge with. This will help us now.

Back in last installment, in the DDDrawPoly() function, we built the polygon the user drew, and then added it to the drawing by calling DLApnd(NULL, p). When we feed DLApnd() NULL as the first argument, it adds the entity (the second argument) to the main drawing list. In practice, this means it ends up on whatever sheet is the current sheet. For some tools, that may be fine, but for this to work properly and be able to merge as it should, we really need it to go to predefined sheets, this is also important for making the tools work with the standard dungeon tools.

So, what I opted to do is to figure out if the drawing contained a sheet called FLOORS or not. If it exists, use it, if not, create a new one. I opted to modify my FindSheet() utility function so it also can create the sheet if necessary and requested. This function now looks like this:

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

	// 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);


	// If we didn't find the sheet and create is true, we make and return it
	if (!result && create) {

		// Creating the basic entity
		SHEET sh;

		// Setting the sheet name
		strcpy_s(sh.SName, sheetName.c_str());

		// Setting the default CStuff data
		sh.CStuff.ERLen = sizeof(SHEET);
		sh.CStuff.EType = ET_SHEET;
		GetCStuff(&sh.CStuff);

		// Indicating that this is a new sheet, and that it should be visible
		sh.Status = SHT_VIS | SHT_NEW;

		// Add the sheet to the main drawing list, getting the entity record back
		pENTREC newSheet = DLApnd(NULL, (pENTREC)&sh);

		// Creating a sublist for the sheet to store the entities it contain
		// Note that this moves the record in memory, so the pointer we got
		// above is now invalid. The call returns the new pointer however.
		newSheet = DLMakeSubList(newSheet);

		// And finally return the entity record for the sheet we just created
		// We don't need to cast this, as DLApnd()/DLMakeSubList() do actually return
		// entity record pointers, in contrast to DLScan() which returns a DWORD
		return newSheet;
	}


	// 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;

}

 

In my DDDrawPoly() function, all I now did was to change the single line calling  DLApnd() to this

	// Finds the entity record for the FLOORS sheet. If it doesn't exist,
	// we request it being created for us so this call will always return a sheet
	pENTREC sheet = FindSheet("FLOORS", true);

	// Getting the sublist for the sheet. This is the drawing list we will insert the
	// entity into
	auto sl = DLGetSubList(sheet);

	// Insert the entity into the sublist we found above.
	pENTREC ent = DLApnd(sl, p);

 

To make sure to always use the BACKGROUND (FLOOR 1) layer, I also needed to find the id for the layer, or create it if it didn’t already exist. This is handled with a few simple lines.

	// Specifies the name of the layer we need
	char* floorlayer = "BACKGROUND (FLOOR 1)";
	
	// Retrives the internal id number for the layer. It is this number that is stored
	// in CStuff
	int lnr = GetLayerNr(floorlayer);
	
	// Checking if we got a valid id back, if not, we need to create the layer
	if (!lnr) {
		//Add a new layer using the same name we were looking for. The id is
		// returned by the call
		lnr = AddLayer(floorlayer);
	}

	// Update CStuff to add the correct layer id
	p->CStuff.ELayer = lnr;

 

Next, I’ll set up walls around our floor. How to handle sheets and layers will be the same as we have done with the floors above, so I won’t talk any more about that except in code comments.

Walls

Who wouldn’t love some walls around their dungeon floors. They have all kinds of useful purposes, like preventing people from walking off the floors and falling into the void below.

Adding walls are actually rather simple. All we need to do to make a set of walls is to copy our floor entity from the FLOORS sheet to the WALLS sheet, and change it to an hollow entity with an outline instead of a filled poly. This is simply a matter of changing the line width. In the DDDrawPoly() function, you can find that I actually explicitly forced the line with to 0, because if I didn’t, it would use the users current line width, which doesn’t make sense for floors. For my walls, I am forcing it to 2 for now at least. I could be using the current settings, which would have offered more flexibility for the user, but also more confusion.

The code below shows how I solved copying and adjusting the walls. Keep in mind that this snippet refers to an editEntity variable, which is an entity we discussed in the last article, and at this point, basically is our new combined floor.

	// Find our WALLS sheet, create a new one if it doesn't exist
	pENTREC sheetF = FindSheet("WALLS", true);

	// Get the sublist from our sheet so we can copy the walls to it
	auto sl = DLGetSubList(sheetF);

	// Find entities on the WALLS sheet using our helper function
	// We then erase what we find, because we will simply create new walls.
	DWORD oldWalls = DLScan(sl, DDMergeFindEntity, DLS_Std, NULL, NULL);
	if (oldWalls != 0) { DLErase((pENTREC)oldWalls); }

	// Create a copy of our floors, and place the copy in the WALLS sheet sublist
	// Note that this entity will have the same entity tag as the original, we must fix that
	pENTREC wallsEntity = DLCopy(sl, editEntity);

	// We create a new temporary entity and populate the CStuff. The reason for doing this
	// is that the new entity gets a new entity tag, and we can steal that for our walls
	// Temporary entity won't be kept, so taking it's tag is fine
	POINT2 tempEnt = { {sizeof(POINT2), ET_POINT2} };
	GetCStuff(&tempEnt);
	wallsEntity->CStuff.Tag = tempEnt.CStuff.Tag;
	
	// Sets the width of the walls to 2 map units
	wallsEntity->CStuff.LWidth = 2;

	// As when drawing the floor, we find the layer id for WALLS, and if it doesn't exist
	// we just create it, and then assign the ID to the ELayer property of our walls
	char* wallslayer = "WALLS";
	int wlnr = GetLayerNr(wallslayer);
	if (!wlnr) {
		wlnr = AddLayer(wallslayer);
	}
	wallsEntity->CStuff.ELayer = wlnr;
	
	// Abusing the pen thickness value to create an identifier so we can recognize
	// our own entities without affecting other entities on the sheet
	// Pen thickness is used when working with Plotters, and isn't used by modern CC3+
	wallsEntity->CStuff.EThick = 76;


	// Update the screen with the edited entity and the walls entity
	EDraw(editEntity);
	EDraw(wallsEntity);

	// Forces a redraw. This is required if we want to see the effects applied to
	// our updated drawing. This slows down drawing obviously, so if you can live
	// with the flat look until the next manual redraw than leave this out
	// but it does look much more impressive with this here.
	Redraw();

 

Playing Nice

When making new entities like this, we want to make sure it plays nice with existing stuff. We’ve already taken care of some of that, namely the sheets and layers. Since we now are using the correct ones here, it means that if we use the regular Dungeon Designer CORRIDOR command, the corridor can actually connect to our rooms, and even break the walls. For all practical purpose, our merged polygons behave just as a single large DD3 room. So we can check that box.

However, our original version from last installment had a major flaw, it couldn’t recognize it’s own entities. We just assumed that no other command had placed anything on the FLOORS sheet. Now, that’s a pretty bad assumption. So, for this installment, I’ve abused the old pen thickness property. Pen thickness is something only used by plotters, and I don’t think many people will print modern CC3+ maps on a plotter. So what I did here is that I just set it to a magic number (76) for all the entities I create, as well as change the DDMergeFindEntity() function to only look for entities with this magic number. That way, we can continue drawing on our dynamic dungeon even if we have also added dungeon rooms via other commands or drawing tools.

There’s one more lesson here though, and that is how to create dynamic previews of the polygons as we draw them. You might remember from the previous installment that we had a rubberband line going from the last node to our cursor, but the rest of the entity in progress was basically invisible. That’s not very user friendly, now is it? So what we want is the same behaviour as we get when we normally draw a poly in CC3+, a proper outline of the work in progress. And that can be done via a dynamic cursor function.

You may remember that our RDATA request looked like this

RDATA NextPolyPointReq =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)&tempPoint,
(DWORD*)&nextNode,RDC_RBAND, DDPolyGetNode, NULL, NULL, 0, NULL, 0 };

RDC_RBAND here is a call to a built-in dynamic cursor function, but we can write our own. The signature of this is a follows

int XPCALL DynamicCursor(float X, float Y, int screenX, int screenY);

X and Y here are map coordinates, and what we are interested in, but we can also grab the screen coordinates here if we need them. Note that this function is called to update the preview every time we move our mouse, so consider what you put in here. Now, let us get a proper function for drawing our polygon. Notice in the RDATA section that we now call our function. The DYNCSR2 macro just casts the function address for you.

RDATA NextPolyPointReq =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)&tempPoint,
(DWORD*)&nextNode,DYNCSR2(PolyCursor), DDPolyGetNode, NULL, NULL, 0, NULL, 0};


// Dynamic Cursor function. Used to generate the drawing preview when we draw
// our polygon in the drawing window.
int XPCALL PolyCursor(float X, float Y, int screenX, int screenY) {

	// Our preview needs to be a path, which is internally represented as a GPATH
	// type. This has undefined space for the node list, so we reserve some memory
	// for this purpose. We use the vector trick which we have discussed earlier
	// Note that we use a standard GPATH2 type here, and not a full entity. The GPATH2 type 
	// is the backing type for a PATH2 entity. (The number 2 means 2-dimensional)
	const auto len = static_cast<int>(sizeof(GPATH2) + sizeof(GPOINT2) * MAX_NODES);
	std::vector<char> buffer(len);
	GPATH2* p = reinterpret_cast<GPATH2*>(buffer.data());

	// Should the path be closed? We should only close it if it contains 3 nodes or more,
	// with only two nodes we will get a line going back and forth which cancels each other
	// out (previews are drawn using an XOR operation. This is great for making them visible
	// on any background, but have the disadvantage that two on top of each other will
	// cancel each other out)
	// Note that you may not enjoy the closed preview, it can be a bit confusing and behaves
	// different from the normal CC3+ drawing preview. If so, just lock this to 0.
	int close = nodecounter > 1 ? 1 : 0;

	// Flags need to be 0. Setting NL_CLS (for closed) seems tempting, but doesn't
	// work with dynamic cursors
	p->Flags = 0;

	// Start at the beginning (0)
	p->SParm = 0;

	// End at the end of the node list. For an open entity, this should be one less than the
	// number of nodes, but we need one extra for the position of the mouse cursor, which
	// do figure int he preview, but isn't a node yet. Also need an extra node if we need to
	// close, since this must be done manually by adding a last node at the starting point
	p->EParm = (float)nodecounter + close;

	// Count is number of nodes, but we need one more for the mouse location, and optionally
	// one for the closing one
	p->Count = nodecounter + 1 + close;
	
	// Copy the nodes from our working list to the path
	for (int cnt = 0; cnt < nodecounter; cnt++) {
		p->Nodes[cnt] = pointList[cnt];
	}

	// Create a point representing our mouse cursor location, populate it with the X and Y
	// values provided, and add this to the node list
	GPOINT2 cursor;
	cursor.x = X;
	cursor.y = Y;
	p->Nodes[nodecounter] = cursor;

	// If we should create a closed poly, we make a copy of the first node at the end
	if (close) p->Nodes[nodecounter + 1] = p->Nodes[0];

	// This call draws the preview on screen
	DPath2(p, 0);

	// The return value specifies the cursor type to use. We tell it to use a crosshair
	return RDC_XHOPT;

}

 

Future Plans

That’s it for this installment. But lots of work still remains. So far we only support drawing with Polygons, but maybe a simple rectangle would be easier in many cases? And we still cannot remove anything from our dungeon, only add on to it. Holes are also not possible yet, if you try to draw a shape that will return in an interior hole, we end up crashing CC3+. Holes are actually a bit tricky, but no matter what we end up with, a crash is not an acceptable result, so we need to fix that.

 

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.

Leave a Reply