C# Shortcuts: C++ Technical Documentation Writer

Background

My C# Shortcuts app is a personal project of mine being developed in my own time outside of work in 2021. The goal of this app is to automate a bunch of different repetitive tasks in my everyday life to save time and increase efficiency. Programmers are definitely one of the laziest people you’ll meet, in the way that if we can get out of manually doing the work, we’ll dedicate 12 hours to automating a 1 hour job, with the aim of not having to manually repeat that 1 hour job in future. That is the goal of this app.

But why C# if my main work is all in C++? The reason is quite simple: because if I wrote it in C++, I wouldn’t be learning anything. C# was the first programming language I used back when I started my tertiary education at AIE, and I’ve touched on it again and again in the years since but I’ve never held onto a C# project long enough to become properly proficient in the language. By building this project in C#, I learn new things with every new feature, and I continue to add to my personal knowledge base, which is always the main goal of all my work.


Theme: Automation
Engine: MonoGame (XNA)
Platform: PC
Time Worked: 20 March 2021 - 11 April 2021

C++ Technical Documentation Writer

One aspect of being a programmer I don’t like is writing technical documentation. Therefore, it only seems natural that I should automate the process as much as I possibly can.

The goal of this feature was to gather information on classes, structs, enums, variables, functions, etc from all of the .h files in a C++ (UE4) project and translate that data into a human readable format in a similar manner to Unreal Engine 4’s online documentation. The projects that I worked on at work were becoming big enough that I keep forgetting exactly where to find some functions or features, not to mention, if something happens to me, someone is going to have to try and interpret all of my work to pick up where I left off. This will also come in handy as I’ve been playing with the idea of releasing some of my personal work to the UE4 Marketplace, and it will save time if I can automate the process.

Reading the text from the files was the easy part – the hard part was interpreting the data. Of course in C++ there are multiple ways of writing the same thing in a header, and so it was a challenge to refine the automation to determine what was garbage data and what was useful information. Even now it’s still not perfect, but as I notice little things I’ll gradually fix them and it’ll make it one step closer to perfection.

What the feature specifically does:

  1. Takes in a folder path and finds all .h files in this folder and all its children folders, and all their children folders recursively.
  2. Then one file at a time, it reads the text data into a string array.
  3. And then for each line:
    1. It assumes the lines are garbage until it finds one that contains “/*”, “UCLASS”, “class” (but not containing “include” or ‘;’), or UENUM, or “enum” (but does not include ‘;’), or “USTRUCT” or “struct” (but not containing “;”), or “namespace” or “UINTERFACE”
    2. Then it finds what we’re dealing with (class, struct, enum, namespace [interfaces are treated as classes]) and then it gets the module, header and include data from the file, and then if it’s a class, struct, or namespace, it checks if it has a description first, and if not moves on to UE4 descriptors, and if not, then name and parents; if it’s an enum, it builds a CppEnum Object with all of the above and then continues to the next line of the string array
    3. The following is all laid out as “else if”s in the code:
      1. If it’s not an enum, it then checks for “GENERATED” and “BODY”, so that it can ignore this line
      2. Else if the line contains “UCLASS” and we’re working on a CppClass Object, if gathers the Descriptors and Meta Descriptors for the class
      3. Else if the line contains “USTRUCT” and we’re working on a CppStruct Object, it gathers the Descriptors and Meta Descriptors for the struct
      4. Else if the line contains “class”, we’re working on a CppClass Object, and the current class name is unknown, it gathers the name and parents of the class
      5. Else if the line contains “struct”, we’re working on a CppStruct Object, and the current struct name is unknown, it gathers the name and parents of the struct
      6. Else if the line contains “namespace”, we’re working on a CppNamespace Object, and the current namespace name is unknown, it gathers the name and parents of the struct
      7. Else if the line contains “public”, “protected”, or “private”, it updates the current protection level to use for the following code
      8. Else if the line contains “};” then we’ve reached the end of the class or struct, etc
        1. Known Issue: This does not yet take into account an array declared in a header like TArray<int32> IntArray = { 1, 2, 3 };
      9. Else if the line contains “/” then we add the line to the currently held description for the next CppObject to come along
      10. Else if the line contains “/*” then we add the line to the currently held description for the next CppObject to come along, and if the line does not contain “*/”, then we keep doing so until we reach the end of the comment
      11. Else if the line contains “UFUNCTION” and is a class or struct, it starts a new function in the current CppClass/CppStruct/etc and gathers the Descriptors and Meta Descriptors
      12. Else if the line contains “UPROPERTY” and is a class or struct, it starts a new variable in the current CppClass/CppStruct/etc and gathers the Descriptors and Meta Descriptors
      13. Else if the line contains “(” and “)”, and if the line has a “(” before a “=” then we know it’s a function, and so we gather the rest of the function data: description, module, header, include, const, override, virtual, protection level, pure, static, name, return type, parameters
        1. (Note: it needs the secondary check because a variable like FVector Location = (0,0,0) and a function like void SetVector(FVector InVector = FVector(0,0,0)) need to be distinguished)
      14. Else if the line contains ‘=’ or we are currently building a variable, or (if the next line has more than one word and does not contain “#if”), then we know it’s a variable, and so we gather the rest of the variable data: description, module, header, include, const, static, protection level, name, variable type, default value
  4. Then depending on the settings, it either outputs the information to a .log file (for debugging purposes) or as a series of HMTL files (one file per class/struct/enum/namespace/interface) so that it can be further formatted and read easily by a person.

One thing I have not yet implemented is the ability to handle nested classes/structs/namespaces/enums.

For example, in one of the files, the HTML output looks like this:

<!doctype html>
<html>
<head>
<meta charset"utf-8">
<title>UJO_FileTools</title>
</head>

<body>
<h1>UJO_FileTools</h1><p>Type: Class</p>

<h2>References</h2>
<p>
	Module: UsefulFunctionsPlugin
	<br>Header: /UsefulFunctionsPlugin/Source/UsefulFunctionsPlugin/Public/JO_FileTools.h
	<br>Include: #include "JO_FileTools.h"
</p>

<h2>Info</h2>
<p>
	Description: *
 * 
 
</p>

<h2>Functions</h2>
<table>
	<tr>
		<th>Name</th>
		<th>Return Type</th>
		<th>Protection</th>
		<th>Parameters</th>
		<th>Static</th>
		<th>Const</th>
		<th>Virtual</th>
		<th>Override</th>
		<th>Pure</th>
		<th>Descriptors</th>
		<th>Meta Descriptors</th>
		<th>Description</th>
	</tr>
	<tr>
		<td>CreateNewConfig</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>TArray<FString> Content</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|ConfigTools"</li>
</ul>
</td>
		<td></td>
		<td>Config Functions/*Creates a new config at location <ProjectPath>/@OptionalFilePath using @Content as the contents of the file
* Eg, if the Project Name is Hamlet, saved at C://Hamlet/, OptionalFilePath = "Saved/Config/Windows/"
* it will create a new file: C://Hamlet/Saved/Config/Windows/ConfigFileName.ini
* @Content is then printed to this new file putting each entry on a new line*/</td>
	</tr>
	<tr>
		<td>CreateNewConfig</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>FString Content</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td></td>
		<td></td>
		<td>Creates a new config at location <ProjectPath>/@OptionalFilePath using @Content as the contents of the file
* Eg, if the Project Name is Hamlet, saved at C://Hamlet/, OptionalFilePath = "Saved/Config/Windows/"
* it will create a new file: C://Hamlet/Saved/Config/Windows/ConfigFileName.ini
* @Content is then printed to this new file</td>
	</tr>
	<tr>
		<td>OverwriteConfigContent</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>TArray<FString>& Content</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|ConfigTools"</li>
</ul>
</td>
		<td></td>
		<td>Overwrites the entire contents of an ini file with @Content</td>
	</tr>
	<tr>
		<td>OverwriteConfigContent</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>FString Content</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td></td>
		<td></td>
		<td>Overwrites the entire contents of an ini file with @Content</td>
	</tr>
	<tr>
		<td>DoesConfigExist</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|ConfigTools"</li>
</ul>
</td>
		<td></td>
		<td>Checks to see if the input config exists</td>
	</tr>
	<tr>
		<td>GetUnbrokenConfig</td>
		<td>FString</td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|ConfigTools"</li>
</ul>
</td>
		<td></td>
		<td>Gets a config file's contents as a single string</td>
	</tr>
	<tr>
		<td>GetBrokenConfig</td>
		<td>TArray<FString></td>
		<td>Public</td>
		<td><ul>
<li>FString ConfigFileName</li>
<li>FString OptionalFilePath = "Saved/Config/Windows/"</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|ConfigTools"</li>
</ul>
</td>
		<td></td>
		<td>Gets a config file's contents as an array of strings using each new line as a new array entry</td>
	</tr>
	<tr>
		<td>GetAllLinesWithXFromFile</td>
		<td>void</td>
		<td>Public</td>
		<td><ul>
<li>FString FilePath</li>
<li>FString SearchTerm</li>
<li>TArray<FString>& Results</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|FileTools"</li>
</ul>
</td>
		<td><ul>
<li>DisplayName = "Get All Lines with X from File"</li>
</ul>
</td>
		<td>File functions/*Given a filepath, return any lines in the text file that contain @SearchTerm*/</td>
	</tr>
	<tr>
		<td>GetSubFolders</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString FilePath</li>
<li>TArray<FString>& SubFolders</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|FileTools"</li>
</ul>
</td>
		<td></td>
		<td>Returns an array of all the sub folder names in the input filepath
* The input filepath should be absolute ("E:/HameltGame/Content/3DAssets/*", not the usual "/Game/3DAsets/")</td>
	</tr>
	<tr>
		<td>GetWholeFolderTree</td>
		<td>bool</td>
		<td>Public</td>
		<td><ul>
<li>FString FilePath</li>
<li>TArray<FString>& MasterFolders</li>
</ul>
</td>
		<td>True</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td>False</td>
		<td><ul>
<li>BlueprintCallable</li>
<li> Category = "JO|FileTools"</li>
</ul>
</td>
		<td></td>
		<td>Returns an array of all the sub folder names in the input filepath
* The input filepath should be absolute ("E:/HameltGame/Content/3DAssets/*", not the usual "/Game/3DAsets/")</td>
	</tr>
</table>

</body>
</html>

And this looks like a basic HTML page like:

UJO_FileTools

UJO_FileTools

Type: Class

References

Module: UsefulFunctionsPlugin
Header: /UsefulFunctionsPlugin/Source/UsefulFunctionsPlugin/Public/JO_FileTools.h
Include: #include “JO_FileTools.h”

Info

Description: * *

Functions

Name Return Type Protection Parameters Static Const Virtual Override Pure Descriptors Meta Descriptors Description
CreateNewConfig bool Public
  • FString ConfigFileName
  • TArray Content
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False
  • BlueprintCallable
  • Category = “JO|ConfigTools”
Config Functions/*Creates a new config at location /@OptionalFilePath using @Content as the contents of the file * Eg, if the Project Name is Hamlet, saved at C://Hamlet/, OptionalFilePath = “Saved/Config/Windows/” * it will create a new file: C://Hamlet/Saved/Config/Windows/ConfigFileName.ini * @Content is then printed to this new file putting each entry on a new line*/
CreateNewConfig bool Public
  • FString ConfigFileName
  • FString Content
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False Creates a new config at location /@OptionalFilePath using @Content as the contents of the file * Eg, if the Project Name is Hamlet, saved at C://Hamlet/, OptionalFilePath = “Saved/Config/Windows/” * it will create a new file: C://Hamlet/Saved/Config/Windows/ConfigFileName.ini * @Content is then printed to this new file
OverwriteConfigContent bool Public
  • FString ConfigFileName
  • TArray& Content
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False
  • BlueprintCallable
  • Category = “JO|ConfigTools”
Overwrites the entire contents of an ini file with @Content
OverwriteConfigContent bool Public
  • FString ConfigFileName
  • FString Content
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False Overwrites the entire contents of an ini file with @Content
DoesConfigExist bool Public
  • FString ConfigFileName
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False
  • BlueprintCallable
  • Category = “JO|ConfigTools”
Checks to see if the input config exists
GetUnbrokenConfig FString Public
  • FString ConfigFileName
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False
  • BlueprintCallable
  • Category = “JO|ConfigTools”
Gets a config file’s contents as a single string
GetBrokenConfig TArray Public
  • FString ConfigFileName
  • FString OptionalFilePath = “Saved/Config/Windows/”
True False False False False
  • BlueprintCallable
  • Category = “JO|ConfigTools”
Gets a config file’s contents as an array of strings using each new line as a new array entry
GetAllLinesWithXFromFile void Public
  • FString FilePath
  • FString SearchTerm
  • TArray& Results
True False False False False
  • BlueprintCallable
  • Category = “JO|FileTools”
  • DisplayName = “Get All Lines with X from File”
File functions/*Given a filepath, return any lines in the text file that contain @SearchTerm*/
GetSubFolders bool Public
  • FString FilePath
  • TArray& SubFolders
True False False False False
  • BlueprintCallable
  • Category = “JO|FileTools”
Returns an array of all the sub folder names in the input filepath * The input filepath should be absolute (“E:/HameltGame/Content/3DAssets/*”, not the usual “/Game/3DAsets/”)
GetWholeFolderTree bool Public
  • FString FilePath
  • TArray& MasterFolders
True False False False False
  • BlueprintCallable
  • Category = “JO|FileTools”
Returns an array of all the sub folder names in the input filepath * The input filepath should be absolute (“E:/HameltGame/Content/3DAssets/*”, not the usual “/Game/3DAsets/”)