diff --git a/CMakeLists.txt b/CMakeLists.txt
index 915912af5..568789a0d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -58,6 +58,7 @@ option(
 	"Link dependencies using CMake's find_package and do not use internal builds"
 	${SRB2_CONFIG_SYSTEM_LIBRARIES_DEFAULT}
 )
+option(SRB2_CONFIG_ENABLE_TESTS "Build the test suite" ON)
 # This option isn't recommended for distribution builds and probably won't work (yet).
 cmake_dependent_option(
 	SRB2_CONFIG_SHARED_INTERNAL_LIBRARIES
@@ -76,6 +77,25 @@ option(SRB2_CONFIG_ZDEBUG "Compile with ZDEBUG defined." OFF)
 option(SRB2_CONFIG_PROFILEMODE "Compile for profiling (GCC only)." OFF)
 set(SRB2_CONFIG_ASSET_DIRECTORY "" CACHE PATH "Path to directory that contains all asset files for the installer. If set, assets will be part of installation and cpack.")
 
+if(SRB2_CONFIG_ENABLE_TESTS)
+	# https://github.com/catchorg/Catch2
+	CPMAddPackage(
+		NAME Catch2
+		VERSION 3.1.1
+		GITHUB_REPOSITORY catchorg/Catch2
+		OPTIONS
+			"CATCH_INSTALL_DOCS OFF"
+	)
+	list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras")
+	include(CTest)
+	include(Catch)
+	add_executable(srb2tests)
+	# To add tests, use target_sources to add individual test files to the target in subdirs.
+	target_link_libraries(srb2tests PRIVATE Catch2::Catch2 Catch2::Catch2WithMain)
+	target_compile_features(srb2tests PRIVATE c_std_11 cxx_std_17)
+	catch_discover_tests(srb2tests)
+endif()
+
 # Enable CCache
 # (Set USE_CCACHE=ON to use, CCACHE_OPTIONS for options)
 if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL Windows)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2f4467a32..9352d55ef 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,6 +5,8 @@ if("${CMAKE_COMPILER_IS_GNUCC}" AND "${CMAKE_SYSTEM_NAME}" MATCHES "Windows" AND
 	target_link_options(SRB2SDL2 PRIVATE "-static")
 endif()
 
+target_compile_features(SRB2SDL2 PRIVATE c_std_11 cxx_std_17)
+
 # Core sources
 target_sourcefile(c)
 target_sources(SRB2SDL2 PRIVATE comptime.c md5.c config.h.in)
@@ -289,6 +291,7 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
 endif()
 
 add_subdirectory(sdl)
+add_subdirectory(tests)
 
 # strip debug symbols into separate file when using gcc.
 # to be consistent with Makefile, don't generate for OS X.
diff --git a/src/doomtype.h b/src/doomtype.h
index f6c236e20..e6da5e50c 100644
--- a/src/doomtype.h
+++ b/src/doomtype.h
@@ -17,6 +17,10 @@
 #ifndef __DOOMTYPE__
 #define __DOOMTYPE__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifdef _WIN32
 //#define WIN32_LEAN_AND_MEAN
 #define RPC_NO_WINDOWS_H
@@ -78,7 +82,9 @@ typedef long ssize_t;
 #endif
 	#define strncasecmp             strnicmp
 	#define strcasecmp              stricmp
+#ifndef __cplusplus
 	#define inline                  __inline
+#endif
 #elif defined (__WATCOMC__)
 	#include <dos.h>
 	#include <sys\types.h>
@@ -100,24 +106,6 @@ char *strcasestr(const char *in, const char *what);
 int startswith (const char *base, const char *tag);
 int endswith (const char *base, const char *tag);
 
-#if defined (macintosh) //|| defined (__APPLE__) //skip all boolean/Boolean crap
-	#define true 1
-	#define false 0
-	#define min(x,y) (((x)<(y)) ? (x) : (y))
-	#define max(x,y) (((x)>(y)) ? (x) : (y))
-
-#ifdef macintosh
-	#define stricmp strcmp
-	#define strnicmp strncmp
-#endif
-
-	#define boolean INT32
-
-	#ifndef O_BINARY
-	#define O_BINARY 0
-	#endif
-#endif //macintosh
-
 #if defined (_WIN32) || defined (__HAIKU__)
 #define HAVE_DOSSTR_FUNCS
 #endif
@@ -144,22 +132,24 @@ size_t strlcpy(char *dst, const char *src, size_t siz);
 
 /* Boolean type definition */
 
-// \note __BYTEBOOL__ used to be set above if "macintosh" was defined,
-// if macintosh's version of boolean type isn't needed anymore, then isn't this macro pointless now?
-#ifndef __BYTEBOOL__
-	#define __BYTEBOOL__
+// Note: C++ bool and C99/C11 _Bool are NOT compatible.
+// Historically, boolean was win32 BOOL on Windows. For equivalence, it's now
+// int32_t. "true" and "false" are only declared for C code; in C++, conversion
+// between "bool" and "int32_t" takes over.
+#ifndef _WIN32
+typedef int32_t boolean;
+#else
+#define BOOL boolean
+#endif
 
-	//faB: clean that up !!
-	#if defined( _MSC_VER)  && (_MSC_VER >= 1800) // MSVC 2013 and forward
-		#include "stdbool.h"
-	#elif defined (_WIN32)
-		#define false   FALSE           // use windows types
-		#define true    TRUE
-		#define boolean BOOL
-	#else
-		typedef enum {false, true} boolean;
-	#endif
-#endif // __BYTEBOOL__
+#ifndef __cplusplus
+#ifndef _WIN32
+enum {false = 0, true = 1};
+#else
+#define false FALSE
+#define true TRUE
+#endif
+#endif
 
 /* 7.18.2.1  Limits of exact-width integer types */
 
@@ -387,4 +377,8 @@ unset_bit_array (bitarray_t * const array, const int value)
 
 typedef UINT64 precise_t;
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif //__DOOMTYPE__
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
new file mode 100644
index 000000000..28c4ce492
--- /dev/null
+++ b/src/tests/CMakeLists.txt
@@ -0,0 +1,3 @@
+target_sources(srb2tests PRIVATE
+	boolcompat.cpp
+)
diff --git a/src/tests/boolcompat.cpp b/src/tests/boolcompat.cpp
new file mode 100644
index 000000000..fee40cd36
--- /dev/null
+++ b/src/tests/boolcompat.cpp
@@ -0,0 +1,8 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "../doomtype.h"
+
+TEST_CASE("C++ bool is convertible to doomtype.h boolean") {
+	REQUIRE(static_cast<boolean>(true) == 1);
+	REQUIRE(static_cast<boolean>(false) == 0);
+}