Developing Add-Ons for CC3+ – Part 7: Dynamic Dungeon Tools 3

Our Dynamic Dungeon project is moving along, and for this installment of the series, I am going to address several interesting concepts and techniques.

  • Creating custom entities so we can store our settings with the map.
  • Creating dialogs to change settings. Here I also show how we can use owner-drawn lists to draw comboboxes with previews of the fill from the map. We’ll also create macro versions of the settings commands.
  • Accessing the drawing InfoBlock to find fills.
  • Making sure our tool stop at the map border, the same way that CC3+’s drawing tools does.

I am including the interesting bits of code right here in the article, but I have made minor changes all over the code from the previous articles to accommodate some of the new features from this article, so don’t forget to download the complete project from the link at the end of this article.

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

As with the previous installments, here is a short video showing our results so far.


Custom Entities

To be able to store data in our map, we basically need a custom entity type we can insert into the drawing list. We’ll start out with a word of warning here; If we mess up here, we can easily end up corrupting the drawing list in the map, which is basically corrupting the entire map. So, be careful when working with these, and be especially careful if you need to change your entity definition.

We talked about entities in the Entity Basics article where we looked at how to create instances of the standard entity types, and how to walk the drawing list to find them and then manipulate them. At that point, we were mostly focused on visible entities in the map, like lines and circles and symbols. But there are lots of entities that have other purposes. Sheets for example is an entity type. And if the palette is attached to the drawing, it is also represented as an entity in the drawing list. So, for our project, we’ll make a new custom entity type that is used to store the settings we need, for now the selected fill for walls and floors, as well as the desired wall width.

Before we get into creating our entities, let us look at some considerations and prerequisites.

First of all, every XP must have an ID. The values 0xF000-0xFFFF is reserved for unregistered XP’s (registered numbers are only available for commercial and supported XP’s), and the default ID most XP’s are set up with is 0xF000. Now, having several XP’s share the same ID isn’t a problem, UNLESS the XP offers custom entities. If it does, the ID must be unique among all the installed XP’s on all the installations we expect the map file with our custom XP to be loaded on. Now, most CC3+ maps are only loaded on the mappers own system, but we shouldn’t take that for granted. Unfortunately, registered numbers won’t be available to us, so the best we can do is to pick a random number and hope for the best. For this project, I’ve chosen the number 0xFDD0, which means you should never use this for any project of your own, instead pick another one.

In the code snippet below, from the XP initialization, you can see I’ve set the ID to 0xF001 as an example.

XP MyXP = { 0, CList, PList, 0, 0, 0, 0xF001, 0, 620, 0, 0, 100 };

The second thing that is absolutely required is an entity service function. This is called by CC3+ for all kind of different actions behind the scenes that could affect the entity. If this entity service function doesn’t exist or isn’t set up properly, CC3+ will crash when trying to save the drawing, if not sooner. I’ll present the actual function shortly, but here’s the prototype and how to register it with your XP:

DWORD XPCALL ExtendSVC(void);
XP MyXP = { 0, CList, PList, 0, 0, (PCMDPROC)ExtendSVC, 0xF001, 0, 620, 0, 0, 100 };

Now, when we are making custom entities, all these will appear as entity type 4 to CC3+. Entity type 4 is a special entity type which is designed to represent custom entities, and will never be used on it’s own. But this affects how we discover appropriate entities when we walk the drawing list. Instead of just looking for the entity type, we also need to check the ID of the XP the entity is registered with, and the XT id, which is an unique identifier for each entity type internal to each XP.

Because of this, custom entities will always begin with a fixed set of fields. The first field is CStuff, which all entities have, followed by an unsigned short to hold the XP Id and a char to hold the internal XT id. Finally, there is also an unsigned int used for a version numbering field, which we can use to detect if a drawing contains an entity based on an old version of our entity definition. The latter is especially important, because memory is allocated based on the size of the entity definition when the entity was created, so if we blindly starts using an entity based on an old definition, we’ll very easily corrupt the drawing. It is important to point out here that the XP toolkit assumes you know exactly what you are doing, it is not very forgiving when it comes to mistakes.

After the mandatory fields, you can more or less add whatever you want, but keep in mind entities are not intended to store huge amount of data. Don’t go around putting binary data like images in there.

 

Our entity definition looks like this:

#define XPID_DDSETTINGS (0xF001)
#define XT_DDSETTINGS 1
#define DDSETTINGS_VERSION 2
typedef struct _tag_ddsettings
{
	CSTUFF		CStuff;		// entity properties
	unsigned short	XPId;		// XP ID # for custom entity SVC
	char		XType;		// entity sub-type (if needed)
	unsigned	Version = DDSETTINGS_VERSION;
	short		EFfloor;	// Floor Fill
	short		EFwall;		// Wall Fill
	float		WWidth;		// Wall Width
} DDSETTINGS;

Note the constants above the definition. These are basically defined to make it easy to initialize the entity correctly when we create instances. The first one, XPID_DDSETTINGS should be our XP ID. It is basically another name for the same value, but it makes it easier when we create entity instances since we have a meaningful name. Keep in mind that all entities in the same XP should have the same identical value here. The second constant, XT_DDSETTINGS is for the locally unique ID. This is just unique locally in this XP, and it is common just starting on 1 for your first entity type, then 2 for the second, and so on. I’ve also defined a constant to keep track of the version. If I ever change the entity, I just bump the version number, and checks I have elsewhere in the code will be able to determine if the entity it encounters is based on an older version of the definition. If you do go this approach, remember to bump this field on every change, especially if your change causes the entity to take up more memory than before.

Now, just creating an entity isn’t enough, we must also tell CC3+ about it. From earlier, you may remember that the entity definition in CC3+ (pENTREC) is a union structure built from all the different entity types. What we need to do is to introduce our own version of this union, instead of the original one. The original one is found in _ESTRUC.H, and if you look at line 903 in that file, you’ll notice that it looks for a flag, USING_CUSTOM_ENTITIES. If this flag is set, it skips defining pENTREC itself, and leaves that to you. This is done because you can’t simply redefine the definition, so this allows you to simply provide your own definition instead. There is one thing to watch out for here though, because CC3+ already redefines this union, in the file XT_Entities.h, and this file does not define a skippable definition. That means that if we want to define our own, we must NOT include XT_Entities.h. Of course, in C++, each source code file have its own includes, so some files can use the CC3+ custom entities, and other files can use our definition, just make sure that all files that will work on your own entities have your definition, while everywhere you need to process custom entities from CC3+ uses the definition from XT_Entities.h

Here’s my setup for this, note that our custom entities will normally also be defined in the same file:

#pragma pack(push, 1)
// Telling CC3+ that we are using custom entities, so it needs to use our own definition
// here in this file, instead of the global definition for pENTREC
#define USING_CUSTOM_ENTITIES

extern "C"
{
#pragma warning( disable : 4200 )
#include <_estruc.h>
}

// Defines the pENTREC union. You'll find the standard definition in _ESTRUC.H, but since
// we are adding custom entities, we must make our own definition. Notice that almost all
// the entities here are the standard CC3+ ones, with our lone one added at the bottom.
typedef union _tag_pentrec {

	CSTUFF        CStuff;

	POINT2        Point;
	LINE2         Line;
	PATH2         Path;
	XPENT         XPEnt;
	TXT2          Txt;
	CIR2          Cir;
	ARC2          Arc;
	ELP2          Elp;
	ELA2          ElA;
	ACT2          Act;
	MPOLY2        MPoly;
	PART          Part;
	DIML2N        DimL2N;
	DIMA2N        DimA2N;
	DIMC2N        DimC2N;
	DIMO2N        DimO2N;
	SYMREF        SymRef;
	SYMDEF        SymDef;
	ATRIB2        Atrib;
	SHEET         Sheet;
	CTRLP         CtrlP;
	WALL2         Wall;
	NOTE          Note;
	XREF          XRef;
	LINE3         Line3;
	PATH3         Path3;

	DDSETTINGS    DDSettings;

} *pENTREC;

#pragma pack(pop)

// Macros for easy casting an entity to the desired type. These return null if the entity is
// not the desired type, otherwise return the cast entity.
#define XT_GENERIC_CAST_TYPE(p, XPIDVAL, XTIDVAL, type) ((p->CStuff.EType==ET_XP &&        \
        p->DDSettings.XPId==(XPIDVAL) && p->DDSettings.XType==(XTIDVAL)) ? ((type*)p)      \
        : nullptr)
#define DD_SETTINGS_CAST(pEntRec)         XT_GENERIC_CAST_TYPE(pEntRec, XPID_DDSETTINGS,   \
        XT_DDSETTINGS,          DDSETTINGS         )



BOOL DDSettingsSVC(pENTREC pEntRec, int ServiceID, int SubID, int DrawMode, 
                                                        GPOINT2* PickPt, DWORD* ReturnValue);

With that in place, we can create instances of our new entity type. Here’s an example from my code

DDSETTINGS dds = { {sizeof(DDSETTINGS), ET_XP}, 
                   XPID_DDSETTINGS, XT_DDSETTINGS, DDSETTINGS_VERSION, 1, 1, 1.0 };
GetCStuff(&dds.CStuff);
DLApnd(NULL, (pENTREC)&dds);

As you can see, I create an instance of the DDSETTINGS struct, and then just append it to the drawing list. This is basically the same thing we do with any entity, but there are a few things to keep in mind here. First of all, in the CStuff section, we need to set the record length correctly. Our entity is quite simple, it doesn’t have any variable size fields like a node list, so the size is simply determined with sizeof. We also need to make sure the entity type is set to 4 (ET_XP). We’ll just skip the rest of CStuff, it will be filled in by the next code line anyway. But there are more fields that need to be filled out, they are in order the XP Id, XT ID and version fields. As you can see, I simply populate these using the constants I defined earlier. Easy enough, but it is very important to set these correctly. Lastly, there are some numbers, these basically sets up default values for the fills and line with (Fill id 1 is always Solid, which is available in any map).

The final part of the puzzle is to implement the entity service function I mentioned earlier. It looks like this, and is basically just a simple check of which entity we are passed, and then the calling of a specific function for that entity type.

DWORD XPCALL ExtendSVC(void) {
	XPENTDATA* pData = GetXPEntData();
	DWORD rc = FALSE;

	// Calls the correct service function for each custom entity type. We only have one
	// but we'll just keep the full framework here for easier adding more entities later
	switch (pData->pEntRec->XPEnt.XType) {
	case XT_DDSETTINGS:
		HelperSetCarry(DDSettingsSVC(pData->pEntRec, pData->nService, pData->nFunction, 
                                                       pData->nDrawMode, pData->pPickPt, &rc));
		return rc;
	}
	return 0;
}

 

And of course, since this function ends up calling DDSettingsSVC, we need to have a look at that too. That function is the service function for the DDSETTINGS entity, and will be called whenever that entity is interacted with. The typical way to build this function is to build it as a large switch  sentence, with cases for all the different possible interactions. For our case, there aren’t many interactions we need to support, mostly because it is a non-drawing entity, so it can’t be edited or split or exploded, so we’ll just return FALSE for almost every case. We do implement the XPEInfo call though, allowing us to return a name that is used for the LIST command.

BOOL DDSettingsSVC(pENTREC pEntRec, int ServiceID, int SubID, int DrawMode, 
                                          GPOINT2* PickPt, DWORD* ReturnValue) {
	
	static char* Name = "Dynamic Dungeon Settings";

	*ReturnValue = 0;

	switch (ServiceID) {
	case XPEDraw:
	case XPEPick:
	case XPEXCheck:
	case XPETran:
	case XPENUTran:
	case XPEMirror:
	case XPEStretch:
	case XPEEdit:
	case XPEExplode:
	case XPES2T:
	case XPET2S:
	case XPECut:
	case XPEAttach:
	case XPEPt2nearT:
	case XPET2Pt:
	case XPETangent:
	case XPEList:
	case XPECurv:
	case XPEDynEd:
		break;

	case XPEClass:
		*ReturnValue = HelperSetXPEClass(C_NONE, H_NONE);
		break;

	case XPEInfo:
		switch (SubID) {
		case 1:
			*ReturnValue = (DWORD)Name;
			break;
		default:
			return TRUE;
		}
		break;

	default:
		ENoSupp();
	}
	return FALSE;
}

 

I’ll just round out the custom entity chapter of this article with the drawing list scanning code I use to check for the correct entity version. This code ensures that we don’t end up using existing instances of our entity based on an older definition, to avoid corruption.

// Scans the drawing list looking for the DDSETTINGS entity. There should ever be only
// one of these, and our code only makes a new one if one doesn't exist, so we assume
// this is true and return the first one we find. (If none are found, code elsewhere
// makes a new one)
pENTREC XPCALL SettingsScan(void* hDList, const pENTREC ent, const DWORD p1, DWORD p2) {

    UNREFERENCED_PARAMETER(p1);
    UNREFERENCED_PARAMETER(p2);
    UNREFERENCED_PARAMETER(hDList);

    // Is the current entity being checked a DDSETTINGS entity? If so we process it
    // (Remember that DLScan calls calls this function once for EVERY entity in the 
    // drawing list, so we use these to weed out all the other types of entities)
    if (DD_SETTINGS_CAST(ent)) {

        // Here we do the version check. If the version of the entity returned matches
        // the version of the current definition, we return it, but if not, we
        // delete it instead to avoid risking corruption from using an old version of the
        // definition and let the scan continue.
        // Since this will lead to no entity returned, code elsewhere makes a new one
        // using the current definition
        if (DD_SETTINGS_CAST(ent)->Version == DDSETTINGS_VERSION) {
            return ent;
        } else {
            DLDelete(ent);
        }
    } 
    return 0;
}

That should be it for our custom entity. As you’ve seen, making the entity itself is quite simple, but it is important to get the framework around it correct. Adding additional entities should be far quicker. I recommend you check out my full solution to see all this working in context.

Settings Dialog

The next step is to make a graphical dialog where people can pick their fill styles for the walls and the floor, as well as selecting the wall width. For this part of the project we’ll dive into Windows SDK territory, as multiple things here rely on that, such as the dialog box itself, including the way we are going to paint the previews in the dialog. For developers more used to working in modern high-level programming languages, the Windows SDK may feel a bit archaic, but just as with the FastCAD SDK, it comes from historic reasons where it is optimized for performance, not developer convenience. I can’t do a full-fledged Windows SDK tutorial here, but I’ll explain the important bits.

Creating the Dialog

I’ve created the dialog itself using the resource editor in Visual Studio. To add a dialog, just right click the project itself in the navigation window, and pick Add -> Resource -> Dialog. Now you can use the toolbox palette to add fields, for my dialog I added 3 labels (Static Text), 2 comboboxes for the fill selectors, and one edit control for the wall width.

Make sure you name the elements, or they’re going to end up with silly names like IDD_DILAOG1, IDC_EDIT1 and so on. You can change their names (ID) from the property sheet. I’ve named my items IDD_DDSETTINGS (The dialog itself), IDC_DDWALLFILL and IDC_DDFLOORFILL (for the comboboxes) and IDC_WALLWIDTH for the wall with edit box. We’ll use these ID’s in the code later, so better have some sensible names.

You’ll also need to change a couple of properties on both combo boxes to prepare them for drawing the previews in them. You’ll need to set the Type to Drop List, and Owner Draw to Fixed.

The actual code for the dialog looks like this (you’ll normally not work directly with the code though)

IDD_DDSETTINGS DIALOGEX 0, 0, 291, 133
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dynamic Dungeon Settings"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,7,112,50,14
    PUSHBUTTON      "Cancel",IDCANCEL,234,112,50,14
    LTEXT           "Floor",IDC_STATIC,17,24,17,8
    LTEXT           "Walls",IDC_STATIC,17,59,18,8
    COMBOBOX        IDC_DDFLOORFILL,71,24,189,30,CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | WS_VSCROLL | WS_TABSTOP
    COMBOBOX        IDC_DDWALLFILL,71,55,189,30,CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | WS_VSCROLL | WS_TABSTOP
    LTEXT           "Wall Width",IDC_STATIC,17,88,40,8
    EDITTEXT        IDC_WALLWIDTH,72,85,51,12,ES_CENTER | ES_AUTOHSCROLL
END

Programming the Dialog

Under the Windows SDK, each window have their own function that handles all interaction with that window, this is known as the window procedure. This window procedure is called every time anything happens in/with that window, for example, it is called when the mouse pointer moves across the area occupied by the window, when we click our mouse button inside the window, when we hit a key on the keyboard inside the window, when the window is opened or closed, or when the window is moved or resized just to mention a few things. Whenever something happens, Windows will send a window message to that window, this message tells us which kind of interaction happened, and associated parameters with that action.

The standard way of implementing such a window procedure is by using a switch statement that switches on the type of message received, and takes appropriate action depending on this message. Windows do have standard ways of handling a lot of things, so we don’t need to write code to handle people moving the dialog on the screen if we don’t want to, but we can actually do so here if we want to.

For our dialog, there are a few cases we need to handle:

  • When the dialog is shown, we need to populate the controls (WM_INITDIALOG)
  • When the dialog is closed, we need to clean up properly. We’ll be using cached images, so if we don’t clean up, we’ll have a huge memory leak (WM_DESTROY)
  • We need to correctly answer the request for the height of the elements in our combo boxes (This is needed for setting up the drawing area properly) (WM_MEASUREITEM)
  • We need to handle drawing each element in the list (WM_DRAWITEM)
  • We need to handle the user clicking on the OK/Cancel buttons (WM_COMMAND)

All other messages can just go to the default handler

I’ve chosen to use GDI+ to handle the drawing of the fill previews. I chose this because it is extremely simple to use, and also gets things like partial transparency correct out of the box. Instead of using GDI+, I could have just the built-in functions from FastCAD, MakeImage() and DelImage() instead. These return an IImage which also works great with Windows drawing operations, and it uses way less memory than GDI+ (The memory cost of using GDI+ is 40MB), but it gets more complicated when you need partial transparency, so I chose to stick with GDI+. Changing between the two is very simple though.

To use GDI+, you need these headers:

#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#pragma comment (lib,"Gdiplus.lib")

 

Now, before we go to the dialog procedure code, I’ll first introduce the function I use to find all the fills in the drawing. Or more precisely, this function only finds the bitmap fills in the drawing. The other types of fills aren’t that relevant for this, so I chose to only get bitmap fills.

While a lot of things in CC3+ is stored as entities in the drawing list, fills are not. They are instead stored in what is called an InfoBlock. This code shows how to access that InfoBlock and parse the Bitmap fills listed there. The important information I need here is the name of the fill, and the path to the image file on disk. This function will be called by our dialog initialization procedure later.

// Struct for storing fill style definitions, as well as the actual image data itself
struct BMPFILL
{
	int fillid;
	char name[128];
	char filename[MAX_PATH];
	Gdiplus::Bitmap * imagefile;
	HBITMAP hBmp;
	bool exist = true;

};
// This function grabs all the bitmap fills defined in the current map and puts them in
// a nice list for us.
std::vector<BMPFILL> GetFillNames() {

    // A vector for stroing the list
    std::vector<BMPFILL> l;

    // Get a hold of the Fill Style Info Block
    // (Things like fill styles aren't stored in the drawing list, but rather in a different
    // structure called an info block. Fill Style is one of these)
    const auto base = (FSTYIB*)DLGetIBAdr(NULL, IB_FSTYLE);

    // Iterates over the fill styles in the info block
    // Each fill style definition store it's own record length, so we use this
    // to increment the pointer to move to the memory location of the next
    // one in the block.
    for (auto os = base->FS; os->RLen != 0; os = (FSTY*)((DWORD)os + os->RLen)) {
        
        // CC3+ has different types of fill styles, but we'll only concern ourselves with
        // the raster (bitmap) ones
        if (os->fstype == FS_BMP) {

            // Sets up an instance of our own data structure to keep the information
            BMPFILL bf;

            // Get hold of the data area of the definition where the relevant information
            // is stored
            const auto pBmp = (BMPSTY*)((DWORD)os + os->dofst);

            // Copies the filename, fill id and fill name to our own structure
            std::string fname(pBmp->bmpname);
            boost::to_lower(fname);
            fname = std::regex_replace(fname, std::regex("_vh|_hi|_vl"), "_lo");
            strcpy_s(bf.filename, fname.c_str());
            bf.fillid = os->ID;
            strcpy_s(bf.name, os->fsname);

            // Add the structure to our list
            l.push_back(bf);
        }
    }

    // Sorting the list so fill styles appear ascending by name in the list
    std::sort(l.begin(), l.end(), 
            [](BMPFILL a, BMPFILL b) {
                return boost::to_lower_copy(std::string(a.name)) 
                                                < boost::to_lower_copy(std::string(b.name)); 
            });

    
    // returns the list
    return l;
}

 

And finally, we have the dialog procedure itself. This function doesn’t interact with the custom entity we made directly, but retrieves the current settings through a helper method, and ensures the new settings are saved as an entity in the map through another helper method. See settings.cpp in my code for the complete details here.

// This is a standard win32 Dialog Window Procedure. Basically, the job of this function is
// to handle everything that may happen to the dialog, from initializing it when it opens
// to drawing the fill previews in the list and reacting when the user clicks the OK or
// Cancel buttons. In the standard Windows SDK, every window have one of these procedures
// connected to it that handles everything for that window.
INT_PTR CALLBACK SettingsDialog(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    
    // The height of the rows in the combo boxes
    static const int rowheight = 24;

    // The width of the image preview
    static const int imagewidth = 80;

    // A vector that holds all the fills in our map. This is static so that it survives
    // between calls to this function.
    static std::vector<BMPFILL> fills;

    // This switch is the meat of the window procedure. It looks at the message Windows
    // send to the window, and decides what should happen based on type of message.
    switch (message) {

        // This happens when the dialog is created (each time it is opened)
        case WM_INITDIALOG:
        {

            // Initializing GDI+. We're using this for easy image handling. This is more
            // or less boilerplate code for this. I could be using the status returned
            // to see if it loaded correctly, but it isn't really helpfull for me at this
            // point. What will happen if it fails to load is that images will fail to load
            // but the dialog will still work. I find this just as useful as telling the
            // user about the failure. Under normal circumstances, it won't fail anyway.
            Gdiplus::GdiplusStartupInput gdiplusStartupInput;
            ULONG_PTR gdiplusToken;
            Gdiplus::Status status = 
                        Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);


            // Populating the fills vector. We do this each time the dialog is opened
            // because the user may have been adding/removing fills in CC3+, so we don't
            // want to rely on cached data
            fills = GetFillNames();

            // We process the list of fills, and load the image for each one into the list.
            // This eats some memory, so it is important that the cleanup process when
            // closing the dialog works properly.
            // We're also preparing the handle to the bitmap which we need in drawing
            // operations later
            for (auto it = fills.begin(); it != fills.end(); it++) {
                char p[MAX_PATH];
                FullFileName((*it).filename, p);
                WCHAR path[MAX_PATH];
                size_t x;
                mbstowcs_s(&x, path, MAX_PATH, p, MAX_PATH);
                it->imagefile = new Gdiplus::Bitmap(path, false);
                if (it->imagefile->GetLastStatus() == Gdiplus::Status::Ok) {
                    it->imagefile->GetHBITMAP(Gdiplus::Color::White, &(*it).hBmp);
                } else {
                    // If we somehow couldn't load the image, we set it as not existing
                    it->exist = false;
                }
            }

            // Retrieve the handles for our two comboboxes
            HWND floorfilllist = GetDlgItem(hwndDlg, IDC_DDFLOORFILL);
            HWND wallsfilllist = GetDlgItem(hwndDlg, IDC_DDWALLFILL);
            
            // Populate the comboboxes. We're just setting the id of the fill for now,
            // and then we'll use that to paint in the correct preview later
            for (auto it = fills.begin(); it != fills.end(); it++) {
                ComboBox_AddItemData(floorfilllist,  (*it).fillid);
                ComboBox_AddItemData(wallsfilllist, (*it).fillid);
            }


            // Retriving the current fills
            DDFILLS dds = GetDDFills();


            // Figuring out which entry in the list is the currently selected fill,
            int floorindex = ComboBox_FindItemData(floorfilllist, -1, dds.floor);
            int wallindex = ComboBox_FindItemData(wallsfilllist, -1, dds.wall);

            // Setting the default value to display in the combobox to match the value the
            // user set last time it was open.
            SendDlgItemMessage(hwndDlg, IDC_DDFLOORFILL, CB_SETCURSEL, floorindex, 0);
            SendDlgItemMessage(hwndDlg, IDC_DDWALLFILL, CB_SETCURSEL, wallindex, 0);

            // Formats the wall width value and puts in in the dialog
            TCHAR buff[25];
            sprintf_s(buff, "%5.2f", dds.width);
            SetDlgItemText(hwndDlg, IDC_WALLWIDTH, buff);

            // Centers the dialog in the application window. This is done by getting
            // the sizes of the two windows and applying some mathematical
            // manipulation to center them.
            HWND hwndOwner;
            RECT rc, rcDlg, rcOwner;
            hwndOwner = GetParent(hwndDlg);
            GetWindowRect(hwndOwner, &rcOwner);
            GetWindowRect(hwndDlg, &rcDlg);
            CopyRect(&rc, &rcOwner);
            OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top);
            OffsetRect(&rc, -rc.left, -rc.top);
            OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom);
            SetWindowPos(hwndDlg, HWND_TOP, rcOwner.left + (rc.right / 2),
                                rcOwner.top + (rc.bottom / 2),    0, 0, SWP_NOSIZE);

            
            return (INT_PTR)TRUE;
        }

        // This case is hit when the dialog is created, we use it to tell Windows
        // the height of each row
        case WM_MEASUREITEM:
        {

            LPMEASUREITEMSTRUCT lpmis = (LPMEASUREITEMSTRUCT)lParam;
            lpmis->itemHeight = rowheight;

            break;
        }

        // This is the case that draws each indivisual item in the combo box list
        // It will be called once for each item, so we'll just be concerned about
        // drawing one item here.
        case WM_DRAWITEM:
        {
            // lParam contains a pointer to a struct containing vital information
            // for our drawing
            LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam;
            if (lpdis->itemID == -1) // Empty item
                break;

            
            // lpdis->itemData contains the data of the element we are currently drawing
            // You may remeber we set it to the id of the fill in WM_INITDIALOG
            // Here we search through the list of fills for the one we need
            // There are more efficient ways of doing this, but the list of fills will
            // allways be realtively small
            BMPFILL bmp;
            bmp.exist = false;
            for(auto it = fills.begin(); it != fills.end(); it++) {
                if ((*it).fillid == lpdis->itemData) {
                    bmp = *it;
                }
            }


            // Draw the name of the fill. the rcItem is a rectangle that contains the 
            // coordinates for our drawing area.
            TEXTMETRIC tm;
            GetTextMetrics(lpdis->hDC, &tm);
            int y = (lpdis->rcItem.bottom + lpdis->rcItem.top - tm.tmHeight) / 2;
            int x = LOWORD(GetDialogBaseUnits()) / 2;
            ExtTextOut(lpdis->hDC, imagewidth + x , y, 
                                        ETO_CLIPPED | ETO_OPAQUE, &lpdis->rcItem, bmp.name, 
                                        strnlen_s(bmp.name,128), NULL);


            // If our bitmap failed to load earlier, we just break out here. This leaves
            // the preview area blank.
            // There are multiple reasons for failing, but the prime one is that the file
            // isn't there (which would lead to red X'es in CC3+)
            if (!bmp.exist) break;


            // A device context is a drawing surface in Windows. lpdis->hDC is the handle
            // to the drawing surface for the actual list, but we need a temporary one
            // to bit our image on.
            HDC hdc = CreateCompatibleDC(lpdis->hDC);
            SelectBitmap(hdc, bmp.hBmp);


            // Copying an appropriate size of the texture image from the temporary DC to
            // the drawing surface for the list.
            BitBlt(lpdis->hDC, 0, lpdis->rcItem.top, imagewidth, rowheight - 2, 
                                                                        hdc, 0, 1, SRCCOPY);


            // Draw a focus rectangle around the item in the list that has the focus
            // (Where the mouse pointer is)
            if (lpdis->itemState & ODS_FOCUS)
                DrawFocusRect(lpdis->hDC, &lpdis->rcItem);
            
            // Deleting our temporary drawing surface. If we don't do this, we'll have
            // memory and resource leaks
            DeleteDC(hdc);

            break;
        }

        // Called when the dialog is closed. We use this case to clean up
        case WM_DESTROY:
        {
            // Iterating through the list of fills and delete the image and
            // image handle, otherwise we leak memory
            for (auto it = fills.begin(); it != fills.end(); it++) {
                DeleteBitmap(it->hBmp);
                delete it->imagefile;
            }
            
            // clear out the list of fills. We don't want it persisting till the next time
            // we open the dialog. Caching would improve performance, but adds complexity
            // since the user can have added or deleted fill styles.
            fills.clear();

            break;
        }

        // Called when the user clicks a button, such as OK or Cancel
        case WM_COMMAND:
        {

            // This switch lets us determine exactly which button
            switch (LOWORD(wParam)) {

            // OK Button
            case IDOK:
            {
                // Grab the handles for our two comboboxes
                HWND floorfilllist = GetDlgItem(hwndDlg, IDC_DDFLOORFILL);
                HWND wallsfilllist = GetDlgItem(hwndDlg, IDC_DDWALLFILL);

                DDFILLS dds;

                // Grabs the data from the selected element. This will be the fill id, which
                // is what we put in earlier
                dds.floor = 
                    ComboBox_GetItemData(floorfilllist, ComboBox_GetCurSel(floorfilllist));
                dds.wall = 
                    ComboBox_GetItemData(wallsfilllist, ComboBox_GetCurSel(wallsfilllist));
                TCHAR buff[12];
                GetDlgItemText(hwndDlg, IDC_WALLWIDTH, buff, 11);
                dds.width = abs(atof(buff));
                

                SetDDFills(dds);

                // Close the dialog
                EndDialog(hwndDlg, wParam);
                return (INT_PTR)TRUE;
            }

            // Cancel button
            case IDCANCEL:

                // Just close the dialog without grabbing any values
                EndDialog(hwndDlg, wParam);
                return (INT_PTR)TRUE;

            }

        }

    }

    return (INT_PTR)FALSE;
}

Calling the Dialog

This is the simplest part of this whole thing. I obviously have to set up a command and all that to make it callable from CC3+, but we’ve done that before, so here’s the simple function our command will call. Note that we get some of the required handles (handle to the instance and the parent window) the dialog call need from the MyXP struct we set up when we initialized this XP back in main.cpp

Note that this is NOT macro-safe due to the dialog, so we will have our own macro commands.

// Entry point for the DDSETTINGS command
void XPCALL DDSettings() {

    // Shows the settings dialog. Notice that we get the application instance and window
    // handle from the MyXP struct which is initialized by the dll registration procedure
    // in Main.cpp
    if (DialogBox(MyXP.ModHdl, MAKEINTRESOURCE(IDD_DDSETTINGS), MyXP.hMainWin, 
                                                                SettingsDialog) == IDOK) {

        // This call to merge will update the dungeon on screen with the new fills
        // immediately instead of waiting for a new segment to be drawn.
        DDMerge();

    } else {

        // Remember to tell CC3+ that the command is done
        // Not required as long as we call DDMerge() but that may change in the future
        CmdEnd();
    }
}

Macro Safe Settings

I always like to be able to configure things using macros. Macros can be assigned to buttons and hotspots and can be loaded from the OnOpenMacro when loading a map and much more. A dialog is nice enough for general use, but the macro versions provide flexibility.

I won’t spend too much time going through these here since they are rather simple commands that simply request data from the user, which we did talk about in the Communicating with the User article. I’d like to point out something here though, when you have the data request set up to request a fill style, you can either type the name of the fill style on the command line, or you can right click to bring up the regular fill style dialog and pick from there. This makes the macro versions of the commands quite user-friendly as an alternative to the dialog we designed above. Another interesting point is that the wall width prompt is designed to show the current value, and allow the user to right click to just accept this one.

Note that I’ve elected to use two commands here, one to set up wall width, and another to set up fills. Macros can get a bit annoing if they keep prompting you for a bunch of things when you just want to change one setting.

// Setting up some temporary variables and the prompts for the requests
// Could have stored data directly to the settings entity, but that causes issues
// if the user abort the command after selecting the first one.
int wtemp, ftemp;
float width = 0;
FORMST(wallFillPrompt, "Fill Style for walls [Dialog]: \0")
FORMST(floorFillPrompt, "Fill Style for floors [Dialog]: \0")
FORMSTPKT(wallWidthPrompt, "Wall Width [!01]: \0",1)
    ITEMFMT(width, FT_Real4, FJ_Var, 0, FDP_User)
FORMSTEND
RDATA wallFillReq =
{ sizeof(RDATA), RD_FStyle, NULL, RDF_C, (DWORD*)&wtemp,
(DWORD*)&wallFillPrompt,RDC_ARROW, DDSettingsRequestHandler, NULL, NULL, 0, NULL, 0 };
RDATA floorFillReq =
{ sizeof(RDATA), RD_FStyle, NULL, RDF_C, (DWORD*)&ftemp,
(DWORD*)&floorFillPrompt,RDC_ARROW, DDSettingsRequestHandler, NULL, NULL, 0, NULL, 0 };
RDATA wallWidthReq =
{ sizeof(RDATA), RD_Real4, NULL, RDF_C, (DWORD*)&width,
(DWORD*)&wallWidthPrompt, RDC_ARROW, DDSettingsWallWidthHandler, NULL, NULL, 0, NULL, 0 };


// Entry point for the DDSETTINGSWALLWM macro command, used to set wall width
void XPCALL DDSettingsWallWMacro() {

    DDFILLS dds = GetDDFills();

    width = dds.width;

    ReqData(&wallWidthReq);

}

// Callback for wall with macro command request
void XPCALL DDSettingsWallWidthHandler(int Result, int result2, int Result3) {

    // If the user supplied an invalid value, which includes hitting Esc to abort, we just 
    // abort the drawing and end the command.
    // Note that if the user accepts the default value, we also do nothing, as this is 
    // the currently active value anyway
    if (Result != X_OK) { CmdEnd(); return; }

    // Valid wall widths are 0 or larger, but CC3+ will accept a negative number on the 
    // command line, so we must handle that case here.
    if (width >= 0) {
        DDFILLS dds = GetDDFills();
        dds.width = width;
        SetDDFills(dds);
        DDMerge();
    }

    CmdEnd();
}


// Entry point for the DDSETTINGSFILLM macro-safe command used to set floor and wall fill.
void XPCALL DDSettingsFillsMacro() {

    wtemp = ftemp = -1;

    // Requesting floor fill from the user. Can be typed in on the command line, or the
    // user can right click to bring up the standard CC3+ fill style dialog
    ReqData(&floorFillReq);
    
}


// Callback for the fill request (used for both wall and floor fills)
void XPCALL DDSettingsRequestHandler(int Result, int result2, int Result3) {

    // 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; }
    
    
    // If wtemp is still less than 0 it means we haven't requested the wall fill yet
    // Note that if the command was aborted, it would also be less than 0, but that
    // will be caught by the Result check above.
    if (wtemp <= 0) {
        ReqData(&wallFillReq);
    } else if (wtemp > -1 && ftemp > -1) {
        // Both temp variables have been set, so the command had completed successfully
        // Just copy them to the real variables, and call DDMerge wich will update the 
        // display with the new selection.

        DDFILLS dds;

        dds.wall = wtemp;
        dds.floor = ftemp;
        SetDDFills(dds);

        DDMerge();
    } else {
        CmdEnd();
    }
}

 

Restricting to the Map Border

As we already know, CC3+’s canvas doesn’t actually have a fixed size, which is why we have introduced the concept of map border to let people control the size of their maps. The existing drawing tools have an option to restrict them to the map border, and here I’ll show you how we can do the same with our custom command.

Doing this consists of two main steps. Step one happens at the start of the drawing command, and finds the extents of the entities on the MAP BORDER layer. Step two happens continously as you move your mouse, and it checks if the coordinates being returned are inside the MAP BORDER extents or not. If they are outside, it simply replaces the outside coordinate with the location of the border.

What it does is simply starting an extents calculation with BgnPExtents(), the we add entities to the list by using EXCheck() and finally, we call EndPExtents() to calculate the extents of the entities we provided. The result will be stored in the GLINE3 we sent in. A GLINE3 struct contains two points, p1 and p2, which refer to the bottom left corner and top right corner of the map respectively.

Warning: Due to the way these sequences of calls are processed internally, it is extremely important that once you start an extents check, you also finish it properly by calling EndPExtents(), or you may end up corrupting your map.

// Stores the extents of the map border. A line has two points, so p1 represents
// the lower left corner of the map (usually 0,0), while p1 represent the upper right corner
GLINE3 BorderExt;

// Finds the extents of the MAP BORDER layer. We'll use this information to ensure the 
// tools only draw within the map border.
void FindBorderExt() {
    BgnPExtents();
    DLScan(NULL, FindBorderEntities, DLS_UNLK | DLS_HSHTOK | DLS_NOWDC | DLS_RO, 0, 0);
    EndPExtents(&BorderExt);
}

// Finds all entities that are on the MAP BORDER layer for the extents check
pENTREC XPCALL FindBorderEntities(hDLIST list, pENTREC entity, const DWORD parm1, DWORD parm2) {

    int layer = GetLayerNr("MAP BORDER");

    if (entity->CStuff.ELayer == layer) {
        EXCheck(entity);
    }

    return 0;
}

In my implementation I just call FindBorderExt() at every start of my drawing commands (for example DDPOLY), ensuring the information is up to date. Then I have two places to check the coordinates, in my dynamic cursor procedure which is responsible for the rubberband preview, and in the actual handler when the user clicks a point. To simplify things, I wrote a simple function that takes in a pointer to a point that just check and update the coordinates in it:

// Check if a point is within the map border, and if it isn't, simply change the 
// point to be on the edge.
void KeepInsideBorder(GPOINT2* point) {
	point->x = point->x < BorderExt.p1.x ? BorderExt.p1.x : point->x;
	point->x = point->x > BorderExt.p2.x ? BorderExt.p2.x : point->x;
	point->y = point->y < BorderExt.p1.y ? BorderExt.p1.y : point->y;
	point->y = point->y > BorderExt.p2.y ? BorderExt.p2.y : point->y;
}

And then in both my dynamic cursor procedure (PolyCursor()) and in my received coordinate handler (DDPolyGetNode()) I just call this to process the received coordinates before adding them to the node list. The example below is from the dynamic cursor procedure

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

 

That’s it for this installment. We didn’t do much to the actual drawing, but we set up a lot of other stuff needed for a proper framework around this. As always, you can download my Visual Studio Solution.

 

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.