code:
#pragma once
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <vector>

// resolution class to store and sort the width and height easily
class resolution
{
public:
	unsigned int width;
	unsigned int height;

	resolution()
	{
		width = 0;
		height = 0;
	}

	resolution(unsigned int width, unsigned int height)
	{
		resolution::width = width;
		resolution::height = height;
	}

	bool operator ==(const resolution &res2)
	{
		return width == res2.width && height == res2.height;
	}

	// in comparisons width takes priority because it's usually displayed first
	bool operator <(const resolution &res2)
	{
		if (width == res2.width)
			return height < res2.height;
		else
			return width < res2.width;
	}
	bool operator >(const resolution &res2)
	{
		if (width == res2.width)
			return height > res2.height;
		else
			return width > res2.width;
	}
};

namespace PluginConfig
{
#pragma pack(push, 1)
	enum ConfigType {
		CONFIG_BOOLEAN,
		CONFIG_NUMERIC,
		CONFIG_STRING,
		CONFIG_DROPDOWN_INDEX,
		CONFIG_DROPDOWN_TEXT,
		CONFIG_DROPDOWN_NUMBER,
		CONFIG_RESOLUTION,
		CONFIG_GROUP_START,
		CONFIG_GROUP_END,
		CONFIG_BUTTON,
		CONFIG_SPACER
	};

	struct PluginConfigBooleanData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		bool defaultVal;
		bool saveAsString;
	};

	struct PluginConfigNumericData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		int defaultVal;
		int minVal;
		int maxVal;
	};

	struct PluginConfigStringData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		LPCWSTR defaultVal;
		bool useUtf8;
	};

	struct PluginConfigDropdownIndexData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		int defaultVal;
		std::vector<LPCWSTR> valueStrings;
	};

	struct PluginConfigDropdownTextData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		LPCWSTR defaultVal;
		std::vector<LPCWSTR> valueStrings;
		bool editable;
		bool useUtf8;
	};

	struct PluginConfigDropdownNumberData {
		LPCWSTR iniVarName;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		int defaultVal;
		std::vector<int> valueInts;
		bool editable;
	};

	struct PluginConfigResolutionData {
		LPCWSTR iniVarName;
		LPCWSTR iniVarName2;
		LPCWSTR iniSectionName;
		LPCWSTR iniFilePath;
		LPCWSTR friendlyName;
		LPCWSTR description;
		resolution defaultVal;
		std::vector<resolution> valueResolutions;
		bool editable;
	};

	struct PluginConfigGroupData
	{
		LPCWSTR name;
		int height;
	};

	struct PluginConfigButtonData {
		LPCWSTR friendlyName;
		LPCWSTR description;
		void(*func)();
	};

	struct PluginConfigSpacerData {
		int height;
	};

	struct PluginConfigOption
	{
		ConfigType cfgType;
		void* data;
	};

	struct PluginConfigArray
	{
		int len;
		PluginConfigOption* options;
	};
#pragma pack(pop)
}

repo_name: twistero/PD

license: mit


code:
#include "framework.h"
#include "PluginConfigApi.h"
#include <detours.h>
#pragma comment(lib, "detours.lib")

#include <windows.h>
#include <iostream>
#include <vector>
#include <strsafe.h>
#include <bassasio.h>
#include <shellapi.h>

void InjectCode(void* address, const std::vector<uint8_t> data)
{
	const size_t byteCount = data.size() * sizeof(uint8_t);

	DWORD oldProtect;
	VirtualProtect(address, byteCount, PAGE_EXECUTE_READWRITE, &oldProtect);
	memcpy(address, data.data(), byteCount);
	VirtualProtect(address, byteCount, oldProtect, nullptr);
}

void NopBytes(void* address, unsigned int num)
{
	std::vector<uint8_t> newbytes = {};

	for (unsigned int i = 0; i < num; ++i) newbytes.push_back(0x90);

	InjectCode(address, newbytes);
}

void resizeInternalBuffers(int frames)
{
	float* oldMixbuffer = divaAudioMixCls->mixbuffer;
	float* oldState2buffer = divaAudioMixCls->state2->buffer;

	divaBufSizeInFrames = frames;

	divaAudioMixCls->mixbuffer = (float*)malloc(divaBufSizeInFrames * 4 * 4);
	divaAudioMixCls->mixbuffer_size = divaBufSizeInFrames * 4 * 4;
	divaAudioMixCls->state2->buffer = (float*)malloc(divaBufSizeInFrames * 4 * 4);
	divaAudioMixCls->state2->buffer_size = divaBufSizeInFrames * 4 * 4;
	divaAudCls->buffer_size = divaBufSizeInFrames;

	free(oldMixbuffer);
	free(oldState2buffer);
	printf("[DivaSound] Resized internal buffers to %d frames\n", frames);
}

void resizeTestLoop()
{
	bool dir = true;
	while (true)
	{
		Sleep(5000);

		if (dir)
			resizeInternalBuffers(divaBufSizeInFrames + 3000);
		else
			resizeInternalBuffers(divaBufSizeInFrames - 3000);

		dir = !dir;
	}
}

void stopPlayback()
{
	if (useAsio)
	{
		if (BASS_ASIO_IsStarted()) BASS_ASIO_Stop();
	}
	else
	{
		if (ma_device_is_started(&device)) ma_device_stop(&device);
	}
}

void closeDevice()
{
	stopPlayback();

	if (useAsio)
	{
		BASS_ASIO_Free();
	}
	else
	{
		ma_device_uninit(&device);
		ma_context_uninit(&context);
	}
}

void audioCallback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
{
	(void)pDevice;
	(void)pOutput;
	(void)pInput;
	(void)frameCount;

	if (divaAudCls->breakDword != 0 || divaAudioMixCls->mixbuffer == nullptr)
	{
		stopPlayback();
		return;
	}

	if (frameCount > divaBufSizeInFrames)
	{
		printf("[DivaSound] Warning: PDAFT buffer is too small\n");

		// allocate a larger buffer if necessary.
		// add 128 frames of padding because this isn't normal and we want to avoid it happening again if possible.
		resizeInternalBuffers(frameCount + 128);
	}

	divaAudioFillbuffer(divaAudioMixCls, (int16_t*)pOutput, frameCount, 0, 0);

	if (bitDepth > 16) // we should generate the output buffer ourselves
	{
		float volumes[4];
		for (int i = 0; i < 4; i++)
		{
			volumes[i] = divaAudioMixCls->volume_master * divaAudioMixCls->volume_channels[i];
		}

		int startChannel = 4 - nChannels;

		for (unsigned int i = 0; i < frameCount; i++)
		{
			for (int currentChannel = startChannel; currentChannel < 4; currentChannel++)
			{
				// note: currentChannel is for the game's buffer, outputChannel is for our output buffer.
				// outputChannel should also be used for volumes.
				int outputChannel = currentChannel - startChannel;

				if (bitDepth == 24) // 24 bit int output
				{
					int32_t out_val = (int32_t)(divaAudioMixCls->mixbuffer[i*4 + currentChannel] * volumes[outputChannel] * 8388607.0f);

					if (out_val > 8388607) out_val = 8388607;
					else if (out_val < -8388608) out_val = -8388608;

					uint64_t out_pos = (i*nChannels + outputChannel) * 3;

					((byte*)pOutput)[out_pos] = out_val;
					((byte*)pOutput)[out_pos + 1] = out_val >> 8;
					((byte*)pOutput)[out_pos + 2] = out_val >> 16;
				}

				else if (bitDepth == 32) // floating point output
				{
					((float*)pOutput)[i*nChannels + outputChannel] = divaAudioMixCls->mixbuffer[i*4 + currentChannel] * volumes[outputChannel];
				}
			}
		}
	}
}

DWORD asioCallback(bool input, DWORD channel, void* buffer, DWORD length, void* user)
{
	ma_uint32 frames = length * 8 / bitDepth / nChannels;
	audioCallback(&device, buffer, NULL, frames);
	return length;
}

void asioNotifyProc(DWORD notify, void* user);

bool asioInit(bool enablePanel)
{
	if (BASS_ASIO_Init == NULL)
	{
		printf("[DivaSound] BASS ASIO not loaded...\n");
		return false;
	}

	if (HIWORD(BASS_ASIO_GetVersion()) != BASSASIOVERSION)
	{
		printf("[DivaSound] Incorrect BASS ASIO version. Use 1.4.0.0.\n");
		return false;
	}

	if (!BASS_ASIO_Init(asioDevice, BASS_ASIO_THREAD))
	{
		printf("[DivaSound] Failed to initialize device\n");
		return false;
	}

	if (enablePanel && showAsioPanel) BASS_ASIO_ControlPanel();

	if (!BASS_ASIO_ChannelEnable(false, 0, (ASIOPROC*)asioCallback, NULL))
	{
		printf("[DivaSound] Failed to enable channels\n");
		BASS_ASIO_Free();
		return false;
	}
	for (int i = 1; i < nChannels; i++)
	{
		if (!BASS_ASIO_ChannelJoin(false, i, 0))
		{
			printf("[DivaSound] Failed to enable channels\n");
			BASS_ASIO_Free();
			return false;
		}
	}

	if (BASS_ASIO_CheckRate(44100))
		BASS_ASIO_SetRate(44100);

	BASS_ASIO_ChannelSetRate(false, 0, 44100);

	if (bitDepth == 32)
		BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_FLOAT);
	else if (bitDepth == 24)
		BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_24BIT);
	else
		BASS_ASIO_ChannelSetFormat(false, 0, BASS_ASIO_FORMAT_16BIT);

	BASS_ASIO_SetNotify((ASIONOTIFYPROC*)asioNotifyProc, NULL);

	return true;
}

void asioReinit()
{
	printf("[DivaSound] Reinitialising ASIO device...\n");
	closeDevice();

	if (!asioInit(false)) return;

	BASS_ASIO_INFO asioInfo;
	BASS_ASIO_GetInfo(&asioInfo);
	double actualRate = BASS_ASIO_GetRate();

	printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", asioInfo.bufpref, (int)(asioInfo.bufpref * 1000 / actualRate), (int)actualRate);

	divaBufSizeInFrames = (int)(asioInfo.bufpref * 44100 / actualRate);
	divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / 44100;
	printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, 44100);

	resizeInternalBuffers(divaBufSizeInFrames);

	if (!BASS_ASIO_Start(asioInfo.bufpref, 1))
	{
		printf("[DivaSound] Failed to start playback device\n");
		BASS_ASIO_Free();
		return;
	}

	printf("[DivaSound] Started playback\n");
}

void asioNotifyProc(DWORD notify, void* user)
{
	if (notify == BASS_ASIO_NOTIFY_RESET)
	{
		std::thread asioReinitThread(asioReinit);
		asioReinitThread.join();
	}
}

bool maInit()
{
	ma_backend backends[] = { maBackend };

	contextConfig = ma_context_config_init();
	contextConfig.threadPriority = ma_thread_priority_highest;

	if (ma_context_init(backends, sizeof(backends) / sizeof(backends[0]), &contextConfig, &context) != MA_SUCCESS) {
		printf("[DivaSound] Failed to initialize context\n");
		return false;
	}

	deviceConfig = ma_device_config_init(ma_device_type_playback);
	deviceConfig.playback.channels = nChannels;
	deviceConfig.sampleRate = 44100;
	deviceConfig.bufferSizeInMilliseconds = requestBuffer; // actual result may be larger
	deviceConfig.periods = nPeriods;
	deviceConfig.dataCallback = audioCallback;
	deviceConfig.pUserData = NULL;
	deviceConfig.playback.shareMode = maSharemode;

	if (bitDepth == 32)
		deviceConfig.playback.format = ma_format_f32;
	else if (bitDepth == 24)
		deviceConfig.playback.format = ma_format_s24;
	else
		deviceConfig.playback.format = ma_format_s16;

	if (nChannels == 4)
	{
		deviceConfig.playback.channelMap[0] = MA_CHANNEL_FRONT_LEFT;
		deviceConfig.playback.channelMap[1] = MA_CHANNEL_FRONT_RIGHT;
		deviceConfig.playback.channelMap[2] = MA_CHANNEL_BACK_LEFT;
		deviceConfig.playback.channelMap[3] = MA_CHANNEL_BACK_RIGHT;
	}

	if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) {
		printf("[DivaSound] Failed to open playback device\n");
		ma_context_uninit(&context);
		return false;
	}
	//printf("[DivaSound] Opened playback device\n");

	return true;
}

void loadConfig()
{
	nChannels = GetPrivateProfileIntW(L"general", L"channels", 2, CONFIG_FILE);
	if (nChannels != 4) nChannels = 2;

	bitDepth = GetPrivateProfileIntW(L"general", L"bit_depth", 16, CONFIG_FILE);
	if (bitDepth != 32 && bitDepth != 24) bitDepth = 16;

	requestBuffer = GetPrivateProfileIntW(L"buffer", L"buffer_size", 10, CONFIG_FILE);
	if (requestBuffer < 0) requestBuffer = 0;

	nPeriods = GetPrivateProfileIntW(L"buffer", L"periods", 2, CONFIG_FILE);
	if (nPeriods < 1) nPeriods = 1;

	useOldInit = GetPrivateProfileIntW(L"general", L"alternate_init", 1, CONFIG_FILE) > 0 ? false : true;

	GetPrivateProfileStringW(L"general", L"backend", L"wasapi", backendName, 32, CONFIG_FILE);
	for (wchar_t& chr : backendName)
		chr = towlower(chr);

	useAsio = false;
	maSharemode = ma_share_mode_shared;

	if (lstrcmpW(backendName, L"asio") == 0)
	{
		StringCchCopyW(backendName, 32, L"ASIO");
		useAsio = true;
	}
	else if (lstrcmpW(backendName, L"directsound") == 0)
	{
		StringCchCopyW(backendName, 32, L"DirectSound");
		maBackend = ma_backend_dsound;
	}
	else if (lstrcmpW(backendName, L"wasapi_exclusive") == 0)
	{
		StringCchCopyW(backendName, 32, L"WASAPI (Exclusive mode)");
		maBackend = ma_backend_wasapi;
		maSharemode = ma_share_mode_exclusive;
	}
	else
	{
		StringCchCopyW(backendName, 32, L"WASAPI");
		maBackend = ma_backend_wasapi;
	}

	asioDevice = GetPrivateProfileIntW(L"asio", L"device", -1, CONFIG_FILE);

	showAsioPanel = GetPrivateProfileIntW(L"asio", L"show_config", 0, CONFIG_FILE) > 0 ? true : false;
}

void hookedAudioInit(initClass *cls, uint64_t unk, uint64_t unk2)
{
	//printf("[DivaSound] Loaded\n");
	printf("[DivaSound] Output config: %S %dch %dbit\n", backendName, nChannels, bitDepth);

	divaAudCls = cls;
	divaAudCls->breakDword = 0;

	if (useOldInit)
	{
		printf("[DivaSound] Using old initialisation method\n");
		// let the game set some stuff up
		// might be necessary sometimes
		divaAudioInit(divaAudCls, unk, unk2);
		//printf("[DivaSound] Game audio initialised\n");
	}
	else
	{
		// setup some variables instead of using the original init function
		divaAudCls->mixer = new audioMixer();
		divaAudCls->mixer->volume_master = 1.0f;
		divaAudCls->mixer->volume_channels[0] = 1.0f;
		divaAudCls->mixer->volume_channels[1] = 1.0f;
		divaAudCls->mixer->volume_channels[2] = 1.0f;
		divaAudCls->mixer->volume_channels[3] = 1.0f;
		divaAudCls->mixer->audioClass = divaAudCls;

		mtx_init(&divaAudCls->mixer->volume_mutex, 2);
	}

	divaAudioMixCls = divaAudCls->mixer;

	divaAudCls->channels = nChannels; // this could replace stereo patch
	divaAudCls->rate = 44100; // really does nothing
	divaAudCls->depth = bitDepth; // setting this to something other than 16 just removes output

	if (useAsio)
	{
		if (!asioInit(true)) return;

		BASS_ASIO_INFO asioInfo;
		BASS_ASIO_GetInfo(&asioInfo);
		double actualRate = BASS_ASIO_GetRate();

		printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", asioInfo.bufpref, (int)(asioInfo.bufpref * 1000 / actualRate), (int)actualRate);

		divaBufSizeInFrames = (int)(asioInfo.bufpref * 44100 / actualRate);
		divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / 44100;
		printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, 44100);

		divaAudioAllocMixer(divaAudioMixCls, unk, unk2, divaBufSizeInFrames);
		printf("[DivaSound] Created internal audio mixer\n");

		if (!BASS_ASIO_Start(asioInfo.bufpref, 1))
		{
			printf("[DivaSound] Failed to start playback device\n");
			BASS_ASIO_Free();
			return;
		}

		printf("[DivaSound] Started playback\n");
	}
	else
	{
		if (!maInit()) return;

		maInternalBufferSizeInMilliseconds = device.playback.internalBufferSizeInFrames * 1000 / device.playback.internalSampleRate; // because miniaudio doesn't seem to have this
		printf("[DivaSound] Output buffer size: %d (%dms at %dHz)\n", device.playback.internalBufferSizeInFrames, maInternalBufferSizeInMilliseconds, device.playback.internalSampleRate);
		printf("[DivaSound] Buffer periods: %d\n", device.playback.internalPeriods);

		divaBufSizeInFrames = device.playback.internalBufferSizeInFrames * device.sampleRate / device.playback.internalSampleRate; // +128; // 128 is just a bit extra in case resampling needs it or something. idk
		divaBufSizeInMilliseconds = divaBufSizeInFrames * 1000 / device.sampleRate;
		printf("[DivaSound] PDAFT buffer size: %d (%dms at %dHz)\n", divaBufSizeInFrames, divaBufSizeInMilliseconds, device.sampleRate);

		divaAudioAllocMixer(divaAudioMixCls, unk, unk2, divaBufSizeInFrames);
		printf("[DivaSound] Created internal audio mixer\n");

		if (ma_device_start(&device) != MA_SUCCESS) {
			printf("[DivaSound] Failed to start playback device\n");
			ma_device_uninit(&device);
			ma_context_uninit(&context);
			return;
		}
		printf("[DivaSound] Started playback\n");
	}

	divaAudCls->hCallback = new HANDLE();
	*divaAudCls->hCallback = (HANDLE)1; // this *should* be an actual thread handle, but I bypassed the destruction stuff anyway lol

	divaAudCls->buffer_size = divaBufSizeInFrames;
	//loopThread = std::thread(resizeTestLoop);
}

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	if (ul_reason_for_call == DLL_PROCESS_ATTACH)
	{
		loadConfig();

		if (useOldInit)
		{
			 // these patches are only needed if calling the game's own init function
			 // they shouldn't be necessary anymore

			// force stereo mode
			InjectCode((void*)0x0000000140A860C0, { 0x02 });

			// remove a call to get device info and skip the check for it
			NopBytes((void*)0x140626b56, 7);
			NopBytes((void*)0x140626b8a, 8);
			InjectCode((void*)0x140626b8a, { 0x48, 0x83, 0xEF, 0x18 }); // fix value of RDI because I changed the flow here
			NopBytes((void*)0x140626ba9, 11);

			// return from the original init early
			InjectCode((void*)0x0000000140626C29, { 0x48, 0xE9 });
		}

		// skip some thread deinitialisation stuff I don't care about
		InjectCode((void*)0x0000000140625F42, { 0xEB, 0x29 });

		DisableThreadLibraryCalls(hModule);
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		DetourAttach(&(PVOID&)divaAudioInit, (PVOID)hookedAudioInit);
		DetourTransactionCommit();
	}
	else if (ul_reason_for_call == DLL_PROCESS_DETACH)
	{
		closeDevice();
	}
	return TRUE;
}

using namespace PluginConfig;

void OpenWiki()
{
	ShellExecuteW(NULL, L"open", L"https://github.com/somewhatlurker/DivaSound/wiki", NULL, NULL, SW_SHOW);
}

PluginConfigOption config[] = {
	{ CONFIG_DROPDOWN_TEXT, new PluginConfigDropdownTextData{L"backend", L"general", CONFIG_FILE, L"Backend:", L"Sets the audio output protocol.", L"WASAPI", std::vector<LPCWSTR>({ L"WASAPI", L"WASAPI_Exclusive" }), true, false } },
	{ CONFIG_DROPDOWN_NUMBER, new PluginConfigDropdownNumberData{ L"channels", L"general", CONFIG_FILE, L"Channels:", L"Sets the number of channels.", 2, std::vector<int>({ 2, 4 }), false } },
	{ CONFIG_DROPDOWN_NUMBER, new PluginConfigDropdownNumberData{ L"bit_depth", L"general", CONFIG_FILE, L"Bit Depth:", L"Sets the audio sample format.\n(32 uses floating point samples)", 16, std::vector<int>({ 16, 24, 32 }), false } },
	{ CONFIG_NUMERIC, new PluginConfigNumericData{ L"buffer_size", L"buffer", CONFIG_FILE, L"Target Buffer Size:", L"Sets the target buffer size in ms.\nWASAPI will often ignore this and adapt to your hardware config automatically.", 10, 1, 100 } },
	{ CONFIG_NUMERIC, new PluginConfigNumericData{ L"periods", L"buffer", CONFIG_FILE, L"Buffer Periods:", L"Sets how often the buffer should be filled.\nFewer periods usually allows for lower latency, but lowering this may cause issues.", 2, 1, 8 } },
	{ CONFIG_BOOLEAN, new PluginConfigBooleanData{ L"alternate_init", L"general", CONFIG_FILE, L"Use new init", L"Use the full initialisation replacement.\nTry unchecking this if DivaSound seems to cause crashes.", true } },
	{ CONFIG_SPACER, new PluginConfigSpacerData{ 8 } },
	{ CONFIG_BUTTON, new PluginConfigButtonData{ L"Help", L"Get help on the DivaSound wiki.", OpenWiki } },
	{ CONFIG_SPACER, new PluginConfigSpacerData{ 8 } },
};

extern "C" __declspec(dllexport) LPCWSTR GetPluginName(void)
{
	return L"DivaSound";
}

extern "C" __declspec(dllexport) LPCWSTR GetPluginDescription(void)
{
	return L"DivaSound Plugin by somewhatlurker\n\nDivaSound replaces the game's original audio output,\nimproving device support and adding configurable options.";
}

extern "C" __declspec(dllexport) PluginConfigArray GetPluginOptions(void)
{
	return PluginConfigArray{ _countof(config), config };
}


code:
#pragma once

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <string>
#include <vector>
#include <thread>
#define MINIAUDIO_IMPLEMENTATION
#include <miniaudio.h>
#include <bassasio.h>
#include <mmdeviceapi.h>
#include <audioclient.h>

void (__cdecl* divaAudioInit)(void* cls, uint64_t unk, uint64_t unk2) = (void(__cdecl*)(void* cls, uint64_t unk, uint64_t unk2))0x1406269F0;

void (__cdecl* divaAudioFillbuffer)(void* mixer, int16_t* buf, uint64_t nFrames, bool disableHpVol, bool invertPhase) = (void(__cdecl*)(void* mixer, int16_t* buf, uint64_t nFrames, bool disableHpVol, bool invertPhase))0x140627370;
// mixer is a pointer to an audioMixer (from audio init's cls + 0x70)
// disableHpVol uses speaker volume for headphones (only works in 4ch mode)
// invertPhase inverts the output signal (only works if disableHpVol == false)

int (__cdecl* divaAudioAllocMixer)(void* cls, uint64_t unk, uint64_t unk2, int64_t nFrames) = (int(__cdecl*)(void* cls, uint64_t unk, uint64_t unk2, int64_t nFrames))0x140626710;
// unks are the same as from the init call. they seem to set the internal mixing channel counts(?)
// cls is the same as mixer in divaAudioFillbuffer
// nFrames is number of audio frames to hold in the mixing buffers (only used when divaAudioFillbuffer is called). Internally this is multiplied by 16 (buffers are built using 32bit floats)

int(__cdecl* mtx_init)(void* mutex, int type) = (int(__cdecl*)(void* mutex, int type))0x14081DEF4;

#pragma pack(push, 1)
struct _50 {
	byte padding00[0x50];
};
struct _70 {
	byte padding00[0x70];
};

struct streamingState {
	std::vector<_50>* state;
	float* buffer;
	uint64_t buffer_size;
};

struct initClass;

struct audioMixer {
	initClass* audioClass;

	std::vector<_70>** state1;
	uint64_t state1_len;
	streamingState* state2;
	uint64_t state2_len;

	float* mixbuffer;
	uint64_t mixbuffer_size;

	void* volume_mutex; // not sure if this is pointer or not
	float volume_master;
	float volume_channels[4];

	byte padding54[4];
};

struct initClass {
	WAVEFORMATEXTENSIBLE wave_format;
	IMMDeviceEnumerator *pEnumerator;
	IMMDevice *pDevice;
	IAudioClient *pAudioClient;

	uint64_t buffer_size;

	IAudioRenderClient *pRenderClient;
	HANDLE hEvent;

	uint64_t channels;
	uint64_t rate; // should always =44100
	uint64_t depth; // should always =16

	audioMixer* mixer;

	HANDLE* hCallback;
	int32_t breakDword;
};
#pragma pack(pop)

// known internal audio mixer variables (used by divaAudioFillbuffer and divaAudioAllocMixer)
// cls + 0x00 = pointer to main audio/init class
// cls + 0x08 = internal state 1 (SE?) vector pointer (or full class?)
// cls + 0x10 = internal state 1 (SE?) length (channels?)
// cls + 0x18 = internal state 2 (streaming?) vector pointer (or full class?)
// cls + 0x20 = internal state 2 (streaming?) length (channels?)
// cls + 0x28 = mixing buffer pointer (when a buffer is generated, this is filled with 32bit floats (four per frame)
// cls + 0x30 = mixing buffer size (frame count * 16)
//
// cls + 0x38 = mutex lock for volume
// cls + 0x40 = master volume float (multiply other volumes by this)
// cls + 0x44 = ch1 volume
// cls + 0x48 = ch2 volume
// cls + 0x4c = ch3 volume
// cls + 0x50 = ch4 volume
//
//
// known main/init audio class variables (used by divaAudioInit)
// cls + 0x0 = WAVEFORMATEXTENSIBLE wave_format (null with old method) (length 0x28)
// cls + 0x28 = IMMDeviceEnumerator *pEnumerator
// cls + 0x30 = IMMDevice *pDevice (default device)
// cls + 0x38 = IAudioClient *pAudioClient
//
// cls + 0x40 = buffer size in frames
//
// cls + 0x48 = IAudioRenderClient *pRenderClient
//
// cls + 0x50 = HANDLE hEvent (for original WASAPI buffer timing)
//
// cls + 0x58 = number of output channels
// cls + 0x60 = audio sample rate
// cls + 0x68 = audio bit depth (only 16bit works)
//
// cls + 0x70 = pointer to mixer class
//
// cls + 0x78 = HANDLE* hCallback (handle to original callback thread)
//
// cls + 0x80 = break flag dword (becomes 1 on exit)

initClass *divaAudCls;
audioMixer *divaAudioMixCls;

std::thread loopThread;

ma_context_config contextConfig;
ma_context context;
ma_device_config deviceConfig;
ma_device device;

int maInternalBufferSizeInMilliseconds;
int divaBufSizeInFrames;
int divaBufSizeInMilliseconds;

int nChannels; // this can only be 2 or 4
int bitDepth; // signed 16/24 bit integer or 32 bit float
int requestBuffer;
int nPeriods;
wchar_t backendName[32]; // wasapi or directsound
ma_backend maBackend;
ma_share_mode maSharemode;
bool useAsio;
int asioDevice;
bool showAsioPanel;

bool useOldInit;

std::wstring ExePath() {
	WCHAR buffer[MAX_PATH];
	GetModuleFileNameW(NULL, buffer, MAX_PATH);
	return std::wstring(buffer);
}

std::wstring DirPath() {
	std::wstring exepath = ExePath();
	std::wstring::size_type pos = exepath.find_last_of(L"\\/");
	return exepath.substr(0, pos);
}

std::wstring CONFIG_FILE_STRING = DirPath() + L"\\plugins\\DivaSound.ini";
LPCWSTR CONFIG_FILE = CONFIG_FILE_STRING.c_str();


code:
#pragma once
#pragma warning(push)
#pragma warning(disable: 4947)

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <string>
#include <fstream>
#include <vector>

#include <msclr\marshal_cppstd.h>

#include "PluginConfigApi.h"

using namespace System;
using namespace System::Windows::Forms;

float BaseScaleSize = 96;
int Col1Width = 110;
int Col2Left = 114;
int Col2Width = 110;
int ConfigBtnLeft = 160;
int ControlSpacing = 6;

// Custom function. Works like GetPrivateProfileIntW but returns bool. Can detect a numeric value or string.
bool GetPrivateProfileBoolW(LPCWSTR lpAppName, LPCWSTR lpKeyName, bool default, LPCWSTR lpFileName)
{
	wchar_t buffer[8];
	GetPrivateProfileStringW(lpAppName, lpKeyName, L"", buffer, 8, lpFileName);
	//MessageBoxW(NULL, buffer, NULL, 0);

	for (wchar_t& chr : buffer)
		chr = towlower(chr);

	bool out;
	if ((lstrcmpW(buffer, L"true") == 0) || (lstrcmpW(buffer, L"1") == 0))
		out = true;
	else if ((lstrcmpW(buffer, L"false") == 0) || (lstrcmpW(buffer, L"0") == 0))
		out = false;
	else
		out = default;

	return out;
}

// declare this early (actual definitions below)
class ConfigOptionBase;
Panel^ MakePanel(int width, int height, std::vector<ConfigOptionBase*> &cfg, ToolTip^ tooltip, bool* hasChanged);

ref class ComboboxValidation
{
public:
	ComboBox^ cb;

	ComboboxValidation(ComboBox^ combobox)
	{
		cb = combobox;
	}

	System::Void CheckNumberLeave(System::Object^ sender, System::EventArgs^ e)
	{
		System::String^ text = cb->Text;

		cli::array<Char>^ digitsarray = gcnew cli::array<Char>{ L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9' };
		System::Collections::Generic::List<Char>^ digitslist = gcnew System::Collections::Generic::List<Char>(digitsarray);

		if (text->Length <= 0)
			return;

		while (text->Length > 0 && text[0] != L'-' && !digitslist->Contains(text[0]))
		{
			text = text->Remove(0, 1);
		}

		if (text->Length <= 1) // can discard up to 1 now because first digit is known good
		{
			cb->Text = text;
			return;
		}

		for (int i = 1; i < text->Length; i++)
		{
			if (!digitslist->Contains(text[i]))
			{
				text = text->Remove(i, 1);
				i--; // negate the upcoming ++
			}
		}

		cb->Text = text;
	}

	System::Void CheckResolutionLeave(System::Object^ sender, System::EventArgs^ e)
	{
		System::String^ text = cb->Text;

		cli::array<Char>^ digitsarray = gcnew cli::array<Char>{ L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9' };
		System::Collections::Generic::List<Char>^ digitslist = gcnew System::Collections::Generic::List<Char>(digitsarray);

		int numX = 0;

		if (text->Length <= 0)
		{
			cb->Text = "x";
			return;
		}

		while (text->Length > 0 && text[0] != L'x' && !digitslist->Contains(text[0]))
		{
			text = text->Remove(0, 1);
		}

		if (text[0] == L'x')
		{
			numX++;
		}

		if (text->Length <= 1) // can discard up to 1 now because first digit is known good
		{
			if (numX == 0) // if that digit wasn't an x though, add one
			{
				text += "x";
			}
			cb->Text = text;
			return;
		}

		for (int i = 1; i < text->Length; i++)
		{
			if (!digitslist->Contains(text[i]) && !(numX == 0 && text[i] == L'x' && ++numX))
			{
				text = text->Remove(i, 1);
				i--; // negate the upcoming ++
			}
		}

		if (numX == 0)
		{
			text += "x";
		}

		cb->Text = text;
	}

};

ref class PluginConfigHandler
{
public:
	Form^ form;

	PluginConfigHandler(Panel^ optspanel, String^ title)
	{
		form = gcnew Form();

		form->Text = title;

		form->FormBorderStyle = FormBorderStyle::FixedDialog;
		form->StartPosition = FormStartPosition::CenterScreen;
		form->MaximizeBox = false;
		form->MinimizeBox = false;
		form->ShowInTaskbar = false;
		form->ShowIcon = false;
		//form->TopMost = true;

		form->BackColor = Drawing::Color::FromArgb(64, 64, 64);
		form->ForeColor = Drawing::Color::White;

		optspanel->Left = 0;
		optspanel->Top = 0;

		Button^ OkBtn = gcnew Button();
		OkBtn->Text = L"OK";
		OkBtn->Left = 4;
		OkBtn->Top = optspanel->Bottom + 4;
		OkBtn->AutoSize = true;
		OkBtn->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		form->ClientSize = Drawing::Size(optspanel->Width, OkBtn->Bottom + 4);

		form->Controls->Add(optspanel);
		form->Controls->Add(OkBtn);
		form->AcceptButton = OkBtn;

		form->FormClosing += gcnew System::Windows::Forms::FormClosingEventHandler(this, &PluginConfigHandler::FormClosing);
		OkBtn->Click += gcnew EventHandler(this, &PluginConfigHandler::OkClick);
	}

	System::Void OpenForm(System::Object^ sender, System::EventArgs^ e)
	{
		// ShowDialog seems to prevent disposal by copying the panel...
		// which would be fine if I didn't rely on using the control handles
		// MessageBox::Show(form->Controls[0]->Handle.ToString());

		//Application::OpenForms[0]->Enabled = false;
		form->Owner = Application::OpenForms[0];
		if (form->Visible)
			form->Hide();
		form->Show();
	}

	System::Void FormClosing(System::Object^ sender, FormClosingEventArgs^ e)
	{
		if (e->CloseReason == CloseReason::UserClosing)
		{
			form->Hide();
			//Application::OpenForms[0]->Enabled = true;
			e->Cancel = true;
		}
	}

	System::Void OkClick(System::Object^ sender, System::EventArgs^ e)
	{
		form->Close();
	}
};

ref class ChangeHandler
{
public:
	bool* changebool;

	ChangeHandler(bool* onchangebool)
	{
		changebool = onchangebool;
	}

	System::Void SetChanged(System::Object^ sender, System::EventArgs^ e)
	{
		*changebool = true;
	}
};

ref class CustomFuncHandler
{
public:
	void(*_func)();

	CustomFuncHandler(void(*func)())
	{
		_func = func;
	}

	System::Void RunFunc(System::Object^ sender, System::EventArgs^ e)
	{
		_func();
	}
};

class ConfigOptionBase
{
public:
	LPCWSTR _iniVarName;
	LPCWSTR _iniSectionName;
	LPCWSTR _iniFilePath;

	LPCWSTR _friendlyName;
	LPCWSTR _description;

	System::IntPtr mainControlHandle;
	bool* hasChanged;

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		return 0;
	}

	virtual void SaveOption()
	{
		return;
	}
};

class OptionMetaGroupStart : public ConfigOptionBase
{
public:
	int _height;

	OptionMetaGroupStart(LPCWSTR friendlyName, int height)
	{
		_friendlyName = friendlyName;
		_height = height;
	}
};
class OptionMetaGroupEnd : public ConfigOptionBase
{
public:
	OptionMetaGroupEnd()
	{
	}
};

class OptionMetaSpacer : public ConfigOptionBase
{
public:
	int _height;

	OptionMetaSpacer(int height)
	{
		_height = height;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		return _height;
	}
};

class BooleanOption : public ConfigOptionBase
{
public:
	bool _defaultVal;
	bool _saveAsString;

	BooleanOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, bool defaultVal, bool saveAsString)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_saveAsString = saveAsString;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		CheckBox^ cb = gcnew CheckBox();

		cb->Text = gcnew String(_friendlyName);
		cb->Checked = GetPrivateProfileBoolW(_iniSectionName, _iniVarName, _defaultVal, _iniFilePath);
		cb->Left = left + 2;
		cb->Top = top;
		cb->AutoSize = true;
		cb->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		// hack to ensure high contrast
		cb->BackColor = System::Drawing::Color::FromArgb(0, 0, 0, 0);

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		cb->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(cb, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		cb->CheckedChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(cb);
		mainControlHandle = cb->Handle;

		int ControlHeight = cb->Font->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		bool boolEnabled = ((CheckBox^)CheckBox::FromHandle(mainControlHandle))->Checked;

		if (_saveAsString)
			WritePrivateProfileStringW(_iniSectionName, _iniVarName, boolEnabled ? L"true" : L"false", _iniFilePath);
		else
			WritePrivateProfileStringW(_iniSectionName, _iniVarName, boolEnabled ? L"1" : L"0", _iniFilePath);
	}
};

class NumericOption : public ConfigOptionBase
{
public:
	int _defaultVal;
	int _minVal;
	int _maxVal;

	NumericOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, int defaultVal, int minVal, int maxVal)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_minVal = minVal;
		_maxVal = maxVal;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		NumericUpDown^ numberbox = gcnew NumericUpDown();

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		numberbox->Minimum = _minVal;
		numberbox->Maximum = _maxVal;

		numberbox->Value = (int)GetPrivateProfileIntW(_iniSectionName, _iniVarName, _defaultVal, _iniFilePath); // cast to int because this returns uint and breaks -1
																												// it seems it does read negative values correctly though... but eventually a parser that properly supports negative numbers would be ideal
		numberbox->Left = left + Col2Left;
		numberbox->Top = top;
		numberbox->Width = Col2Width;
		numberbox->AutoSize = true;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		numberbox->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(numberbox, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		numberbox->ValueChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(label);
		panel->Controls->Add(numberbox);
		mainControlHandle = numberbox->Handle;

		int ControlHeight = numberbox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		System::String^ tempSysStr;
		std::wstring tempWStr;

		tempSysStr = Convert::ToInt32(((NumericUpDown^)NumericUpDown::FromHandle(mainControlHandle))->Value).ToString();
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		WritePrivateProfileStringW(_iniSectionName, _iniVarName, tempWStr.c_str(), _iniFilePath);
	}
};

class StringOption : public ConfigOptionBase
{
public:
	LPCWSTR _defaultVal;
	bool _useUtf8;

	StringOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, LPCWSTR defaultVal, bool useUtf8)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_useUtf8 = useUtf8;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		TextBox^ textbox = gcnew TextBox();

		WCHAR stringBuf[256];
		char utf8Buf[256];

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		GetPrivateProfileStringW(_iniSectionName, _iniVarName, _defaultVal, stringBuf, 256, _iniFilePath);
		if (_useUtf8 && wcscmp(_defaultVal, stringBuf) != 0) { // don't convert default value
			WideCharToMultiByte(CP_ACP, 0, stringBuf, -1, utf8Buf, 256, NULL, NULL); // convert back to the original ANSI as read from file
			MultiByteToWideChar(CP_UTF8, 0, utf8Buf, -1, stringBuf, 256); // now use those bytes to convert from UTF8
		}

		textbox->Text = gcnew String(stringBuf);
		textbox->Left = left + Col2Left;
		textbox->Top = top;
		textbox->Width = Col2Width;
		textbox->AutoSize = true;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		textbox->Scale(ScaleWidth, ScaleHeight);

		// disable editing for utf8 mode because the ANSI hack used may not work correctly
		if (_useUtf8) {
			textbox->Enabled = false;
		}

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(textbox, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		textbox->TextChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(label);
		panel->Controls->Add(textbox);
		mainControlHandle = textbox->Handle;

		int ControlHeight = textbox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		// disable saving for utf8 mode because the ANSI hack used may not work correctly
		if (_useUtf8)
			return;

		System::String^ tempSysStr;
		std::wstring tempWStr;
		WCHAR stringBuf[256];
		char utf8Buf[256];

		tempSysStr = ((TextBox^)TextBox::FromHandle(mainControlHandle))->Text;
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		tempWStr.copy(stringBuf, 256, 0);
		stringBuf[tempWStr.length()] = 0;

		if (_useUtf8) {
			WideCharToMultiByte(CP_UTF8, 0, stringBuf, -1, utf8Buf, 256, NULL, NULL); // convert to UTF8
			MultiByteToWideChar(CP_ACP, 0, utf8Buf, -1, stringBuf, 256); // now convert that to wide chars as if it's ANSI so it saves properly after conversion to ANSI by windows
		}

		WritePrivateProfileStringW(_iniSectionName, _iniVarName, stringBuf, _iniFilePath);
	}
};

class DropdownOption : public ConfigOptionBase
{
public:
	int _defaultVal;
	std::vector<LPCWSTR> _valueStrings;

	DropdownOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, int defaultVal, std::vector<LPCWSTR> valueStrings)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_valueStrings = valueStrings;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		ComboBox^ combobox = gcnew ComboBox();

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		for (LPCWSTR& choice : _valueStrings) {
			combobox->Items->Add(msclr::interop::marshal_as<System::String^>(choice));
		}
		combobox->SelectedIndex = GetPrivateProfileIntW(_iniSectionName, _iniVarName, _defaultVal, _iniFilePath);
		combobox->Left = left + Col2Left;
		combobox->Top = top;
		combobox->Width = Col2Width;
		combobox->AutoSize = true;
		combobox->FlatStyle = System::Windows::Forms::FlatStyle::Flat;
		combobox->DropDownStyle = ComboBoxStyle::DropDownList;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		combobox->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(combobox, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		combobox->SelectedIndexChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(label);
		panel->Controls->Add(combobox);
		mainControlHandle = combobox->Handle;

		int ControlHeight = combobox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		System::String^ tempSysStr;
		std::wstring tempWStr;

		tempSysStr = Convert::ToInt32(((ComboBox^)ComboBox::FromHandle(mainControlHandle))->SelectedIndex).ToString();
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		WritePrivateProfileStringW(_iniSectionName, _iniVarName, tempWStr.c_str(), _iniFilePath);
	}
};

class DropdownTextOption : public ConfigOptionBase
{
public:
	LPCWSTR _defaultVal;
	std::vector<LPCWSTR> _valueStrings;
	bool _editable;
	bool _useUtf8;

	DropdownTextOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, LPCWSTR defaultVal, std::vector<LPCWSTR> valueStrings, bool editable, bool useUtf8)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_valueStrings = valueStrings;
		_editable = editable;
		_useUtf8 = useUtf8;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		ComboBox^ combobox = gcnew ComboBox();

		WCHAR stringBuf[256];
		char utf8Buf[256];

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		for (LPCWSTR& choice : _valueStrings) {
			combobox->Items->Add(msclr::interop::marshal_as<System::String^>(choice));
		}

		GetPrivateProfileStringW(_iniSectionName, _iniVarName, _defaultVal, stringBuf, 256, _iniFilePath);
		if (_useUtf8 && wcscmp(_defaultVal, stringBuf) != 0) { // don't convert default value
			WideCharToMultiByte(CP_ACP, 0, stringBuf, -1, utf8Buf, 256, NULL, NULL); // convert back to the original ANSI as read from file
			MultiByteToWideChar(CP_UTF8, 0, utf8Buf, -1, stringBuf, 256); // now use those bytes to convert from UTF8
		}

		combobox->Text = gcnew String(stringBuf);
		combobox->Left = left + Col2Left;
		combobox->Top = top;
		combobox->Width = Col2Width;
		combobox->AutoSize = true;
		combobox->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		if (_editable)
			combobox->DropDownStyle = ComboBoxStyle::DropDown;
		else
			combobox->DropDownStyle = ComboBoxStyle::DropDownList;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		combobox->Scale(ScaleWidth, ScaleHeight);

		// disable editing for utf8 mode because the ANSI hack used may not work correctly
		if (_useUtf8) {
			label->Enabled = false;
			combobox->Enabled = false;
		}

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(combobox, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		if (_editable)
			combobox->TextChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);
		else
			combobox->SelectedIndexChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(label);
		panel->Controls->Add(combobox);
		mainControlHandle = combobox->Handle;

		int ControlHeight = combobox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		// disable saving for utf8 mode because the ANSI hack used may not work correctly
		if (_useUtf8)
			return;

		System::String^ tempSysStr;
		std::wstring tempWStr;
		WCHAR stringBuf[256];
		char utf8Buf[256];

		tempSysStr = ((ComboBox^)ComboBox::FromHandle(mainControlHandle))->Text;
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		tempWStr.copy(stringBuf, 256, 0);
		stringBuf[tempWStr.length()] = 0;

		if (_useUtf8) {
			WideCharToMultiByte(CP_UTF8, 0, stringBuf, -1, utf8Buf, 256, NULL, NULL); // convert to UTF8
			MultiByteToWideChar(CP_ACP, 0, utf8Buf, -1, stringBuf, 256); // now convert that to wide chars as if it's ANSI so it saves properly after conversion to ANSI by windows
		}

		WritePrivateProfileStringW(_iniSectionName, _iniVarName, stringBuf, _iniFilePath);
	}
};

class DropdownNumberOption : public ConfigOptionBase
{
public:
	int _defaultVal;
	std::vector<int> _valueInts;
	bool _editable;

	DropdownNumberOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, int defaultVal, std::vector<int> valueInts, bool editable)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_valueInts = valueInts;
		_editable = editable;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		ComboBox^ combobox = gcnew ComboBox();

		System::String^ tempSysStr;

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		for (int& choice : _valueInts) {
			combobox->Items->Add(Convert::ToInt32(choice).ToString());
		}

		tempSysStr = Convert::ToInt32((int)GetPrivateProfileIntW(_iniSectionName, _iniVarName, _defaultVal, _iniFilePath)).ToString();

		combobox->Text = gcnew String(tempSysStr);
		combobox->Left = left + Col2Left;
		combobox->Top = top;
		combobox->Width = Col2Width;
		combobox->AutoSize = true;
		combobox->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		if (_editable)
			combobox->DropDownStyle = ComboBoxStyle::DropDown;
		else
			combobox->DropDownStyle = ComboBoxStyle::DropDownList;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		combobox->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(combobox, gcnew String(_description));

		ComboboxValidation^ validation = gcnew ComboboxValidation(combobox);
		combobox->Leave += gcnew System::EventHandler(validation, &ComboboxValidation::CheckNumberLeave);

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		if (_editable)
			combobox->TextChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);
		else
			combobox->SelectedIndexChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(label);
		panel->Controls->Add(combobox);
		mainControlHandle = combobox->Handle;

		int ControlHeight = combobox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		System::String^ tempSysStr;
		std::wstring tempWStr;

		tempSysStr = ((ComboBox^)ComboBox::FromHandle(mainControlHandle))->Text;
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);

		WritePrivateProfileStringW(_iniSectionName, _iniVarName, tempWStr.c_str(), _iniFilePath);
	}
};

class ResolutionOption : public ConfigOptionBase
{
public:
	LPCWSTR _iniVarName2;
	resolution _defaultVal;
	std::vector<resolution> _valueResolutions;
	bool _editable;

	ResolutionOption(LPCWSTR iniVarName, LPCWSTR iniVarName2, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, resolution defaultVal, std::vector<resolution> valueResolutions, bool editable)
	{
		_iniVarName = iniVarName;
		_iniVarName2 = iniVarName2;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_valueResolutions = valueResolutions;
		_editable = editable;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Label^ label = gcnew Label();
		ComboBox^ combobox = gcnew ComboBox();

		System::String^ tempSysStr;

		label->Text = gcnew String(_friendlyName);
		label->Left = left;
		label->Top = top + 3;
		label->Width = Col1Width;
		label->AutoSize = true;
		label->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		for (resolution& choice : _valueResolutions) {
			tempSysStr = Convert::ToInt32(choice.width).ToString() + L"x" + Convert::ToInt32(choice.height).ToString();
			combobox->Items->Add(tempSysStr);
		}

		int width = GetPrivateProfileIntW(_iniSectionName, _iniVarName, -1, _iniFilePath);
		int height = GetPrivateProfileIntW(_iniSectionName, _iniVarName2, -1, _iniFilePath);
		if (width == -1 || height == -1) {
			tempSysStr = Convert::ToInt32(_defaultVal.width).ToString() + L"x" + Convert::ToInt32(_defaultVal.height).ToString();
			combobox->Text = tempSysStr;
		}
		else {
			tempSysStr = Convert::ToInt32(width).ToString() + L"x" + Convert::ToInt32(height).ToString();
			combobox->Text = tempSysStr;
		}

		combobox->Left = left + Col2Left;
		combobox->Top = top;
		combobox->Width = Col2Width;
		combobox->AutoSize = true;
		combobox->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		if (_editable)
			combobox->DropDownStyle = ComboBoxStyle::DropDown;
		else
			combobox->DropDownStyle = ComboBoxStyle::DropDownList;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		label->Scale(ScaleWidth, ScaleHeight);
		combobox->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(label, gcnew String(_description));
		tooltip->SetToolTip(combobox, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		if (_editable)
			combobox->TextChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);
		else
			combobox->SelectedIndexChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		ComboboxValidation^ validation = gcnew ComboboxValidation(combobox);
		combobox->Leave += gcnew System::EventHandler(validation, &ComboboxValidation::CheckResolutionLeave);

		panel->Controls->Add(label);
		panel->Controls->Add(combobox);
		mainControlHandle = combobox->Handle;

		int ControlHeight = combobox->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		System::String^ tempSysStr;
		std::wstring tempWStr;
		cli::array<String^>^ resolutionArray;

		tempSysStr = ((ComboBox^)ComboBox::FromHandle(mainControlHandle))->Text;
		resolutionArray = tempSysStr->Split('x');

		tempSysStr = resolutionArray[0];
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		WritePrivateProfileStringW(_iniSectionName, _iniVarName, tempWStr.c_str(), _iniFilePath);

		tempSysStr = resolutionArray[1];
		tempWStr = msclr::interop::marshal_as<std::wstring>(tempSysStr);
		WritePrivateProfileStringW(_iniSectionName, _iniVarName2, tempWStr.c_str(), _iniFilePath);

	}
};

class ButtonOption : public ConfigOptionBase
{
public:
	void(*_func)();

	ButtonOption(LPCWSTR friendlyName, LPCWSTR description, void(*func)())
	{
		_friendlyName = friendlyName;
		_description = description;
		_func = func;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		Button^ button = gcnew Button();

		button->Text = gcnew String(_friendlyName);
		button->Left = left;
		button->Top = top;
		button->AutoSize = true;
		button->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		button->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(button, gcnew String(_description));

		CustomFuncHandler^ funchandler = gcnew CustomFuncHandler(_func);
		button->Click += gcnew System::EventHandler(funchandler, &CustomFuncHandler::RunFunc);

		panel->Controls->Add(button);

		int ControlHeight = button->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}
};

class PluginOption : public ConfigOptionBase
{
public:
	bool _defaultVal;
	std::vector<ConfigOptionBase*> _configopts;

	PluginOption(LPCWSTR iniVarName, LPCWSTR iniSectionName, LPCWSTR iniFilePath, LPCWSTR friendlyName, LPCWSTR description, bool defaultVal, std::vector<ConfigOptionBase*> configopts)
	{
		_iniVarName = iniVarName;
		_iniSectionName = iniSectionName;
		_iniFilePath = iniFilePath;
		_friendlyName = friendlyName;
		_description = description;
		_defaultVal = defaultVal;
		_configopts = configopts;
	}

	virtual int AddToPanel(Panel^ panel, unsigned int left, unsigned int top, ToolTip^ tooltip)
	{
		CheckBox^ cb = gcnew CheckBox();
		Button^ button = gcnew Button();

		cb->Text = gcnew String(_friendlyName);
		cb->Checked = GetPrivateProfileBoolW(_iniSectionName, _iniVarName, _defaultVal, _iniFilePath);
		cb->Left = left + 2;
		cb->Top = top + 3;
		cb->AutoSize = true;
		cb->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		// hack to ensure high contrast
		cb->BackColor = System::Drawing::Color::FromArgb(0, 0, 0, 0);

		button->Text = L"Config";
		button->Left = left + ConfigBtnLeft;
		button->Top = top;
		button->AutoSize = true;
		button->FlatStyle = System::Windows::Forms::FlatStyle::Flat;

		Form^ RootForm = panel->FindForm();
		float ScaleWidth = 1.0f;
		float ScaleHeight = 1.0f;
		if (RootForm)
		{
			Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
			ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
			ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
		}
		cb->Scale(ScaleWidth, ScaleHeight);
		button->Scale(ScaleWidth, ScaleHeight);

		tooltip->SetToolTip(cb, gcnew String(_description));

		if (hasChanged == nullptr)
			hasChanged = new bool(false);
		ChangeHandler^ changehandler = gcnew ChangeHandler(hasChanged);
		cb->CheckedChanged += gcnew System::EventHandler(changehandler, &ChangeHandler::SetChanged);

		panel->Controls->Add(cb);
		mainControlHandle = cb->Handle;

		if (_configopts.size() > 0)
		{
			ToolTip^ paneltooltip = gcnew ToolTip();
			Panel^ configPanel = MakePanel((Col2Left + Col2Width + 64), 250, _configopts, paneltooltip, hasChanged);
			configPanel->Scale(ScaleWidth, ScaleHeight);
			PluginConfigHandler^ confighandler = gcnew PluginConfigHandler(configPanel, gcnew String(_friendlyName) + " Options");
			button->Click += gcnew System::EventHandler(confighandler, &PluginConfigHandler::OpenForm);

			panel->Controls->Add(button);
		}

		int ControlHeight = button->Height / ScaleHeight;
		return ControlHeight + ControlSpacing;
	}

	virtual void SaveOption()
	{
		bool boolEnabled = ((CheckBox^)CheckBox::FromHandle(mainControlHandle))->Checked;

		WritePrivateProfileStringW(_iniSectionName, _iniVarName, boolEnabled ? L"1" : L"0", _iniFilePath);

		for (ConfigOptionBase* opt : _configopts)
		{
			opt->SaveOption();
		}
	}
};

Panel^ MakePanel(int width, int height, std::vector<ConfigOptionBase*> &cfg, ToolTip^ tooltip, bool* hasChanged)
{
	Panel^ outpanel = gcnew Panel();
	outpanel->Width = width;
	outpanel->Height = height;
	outpanel->AutoScroll = true;

	Form^ RootForm = outpanel->FindForm();
	float ScaleWidth = 1.0f;
	float ScaleHeight = 1.0f;
	if (RootForm)
	{
		Drawing::SizeF CurrentScaleSize = RootForm->CurrentAutoScaleDimensions;
		ScaleWidth = CurrentScaleSize.Width / BaseScaleSize;
		ScaleHeight = CurrentScaleSize.Height / BaseScaleSize;
	}

	int curX = 12;
	int curY = 3;

	for (int i = 0; i < cfg.size(); i++)
	{
		if (typeid(*cfg[i]).hash_code() == typeid(OptionMetaGroupEnd).hash_code())
			continue;

		if (typeid(*cfg[i]).hash_code() == typeid(OptionMetaGroupStart).hash_code())
		{
			OptionMetaGroupStart* groupData = (OptionMetaGroupStart*)(cfg[i]);
			GroupBox^ groupbox = gcnew GroupBox();
			groupbox->Width = width - (ScaleWidth * 8);
			groupbox->Height = ScaleHeight * groupData->_height;
			groupbox->Left = ScaleWidth * 4;
			groupbox->Top = ScaleHeight * curY;
			groupbox->Text = gcnew String(groupData->_friendlyName);

			// find the end of this group by simple iteration keeping track of indent level
			int level = 1;
			int endidx;
			for (endidx = i + 1; endidx < cfg.size(); endidx++)
			{
				if (typeid(*cfg[endidx]).hash_code() == typeid(OptionMetaGroupStart).hash_code())
					level++;
				if (typeid(*cfg[endidx]).hash_code() == typeid(OptionMetaGroupEnd).hash_code())
					level--;

				if (level == 0)
					break;
			}

			Panel^ groupPanel = MakePanel(groupbox->Width - (ScaleWidth * 8), groupbox->Height - (ScaleHeight * 10), std::vector<ConfigOptionBase*>(&(cfg[i + 1]), &(cfg[endidx])), tooltip, hasChanged);
			groupPanel->Left = ScaleWidth * 12;
			groupPanel->Top = ScaleHeight * (curY + 8);

			groupbox->Controls->Add(groupPanel);
			outpanel->Controls->Add(groupbox);

			i = endidx;
			curY += groupData->_height;
		}
		else
		{
			cfg[i]->hasChanged = hasChanged;
			curY += cfg[i]->AddToPanel(outpanel, curX, curY, tooltip);
		}
	}

	return outpanel;
}

#pragma warning(pop)


code:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <Filter Include="Header Files">
      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
      <Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
    </Filter>
    <Filter Include="Resource Files">
      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
    </Filter>
    <Filter Include="Source Files">
      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
      <Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
    </Filter>
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="dllmain.cpp">
      <Filter>Source Files</Filter>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="ui.h">
      <Filter>Header Files</Filter>
    </ClInclude>
    <ClInclude Include="constants.h">
      <Filter>Header Files</Filter>
    </ClInclude>
  </ItemGroup>
</Project>


code:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Debug|x64">
      <Configuration>Debug</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|x64">
      <Configuration>Release</Configuration>
      <Platform>x64</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <VCProjectVersion>16.0</VCProjectVersion>
    <ProjectGuid>{60d5e9f4-335f-402b-9a07-d78674dffc9b}</ProjectGuid>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <Keyword>ManagedCProj</Keyword>
    <RootNamespace>Launcher</RootNamespace>
    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
    <ProjectName>Launcher</ProjectName>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CLRSupport>true</CLRSupport>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CLRSupport>true</CLRSupport>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CLRSupport>true</CLRSupport>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
    <ConfigurationType>DynamicLibrary</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <PlatformToolset>v142</PlatformToolset>
    <CLRSupport>true</CLRSupport>
    <CharacterSet>Unicode</CharacterSet>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings">
  </ImportGroup>
  <ImportGroup Label="Shared">
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <LinkIncremental>true</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <LinkIncremental>true</LinkIncremental>
    <TargetExt>.dva</TargetExt>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <LinkIncremental>false</LinkIncremental>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <LinkIncremental>false</LinkIncremental>
    <TargetExt>.dva</TargetExt>
    <TargetName>$(ProjectName)</TargetName>
  </PropertyGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <PreprocessorDefinitions>WIN32;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>
    <Link>
      <AdditionalDependencies />
      <SubSystem>Windows</SubSystem>
      <EntryPointSymbol>main</EntryPointSymbol>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
    <ClCompile>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>
      </PrecompiledHeaderFile>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <AdditionalIncludeDirectories>..\..\..\dependencies\detours\include;..\..\..\dependencies\freeglut\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
      <LanguageStandard>stdcpp17</LanguageStandard>
    </ClCompile>
    <Link>
      <AdditionalDependencies>User32.lib</AdditionalDependencies>
      <SubSystem>Windows</SubSystem>
      <EntryPointSymbol>
      </EntryPointSymbol>
      <AdditionalLibraryDirectories>..\..\..\dependencies\detours\lib;..\..\..\dependencies\freeglut\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <PrecompiledHeader>Use</PrecompiledHeader>
      <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
      <WarningLevel>Level3</WarningLevel>
      <PreprocessorDefinitions>WIN32;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
    </ClCompile>
    <Link>
      <AdditionalDependencies />
      <SubSystem>Windows</SubSystem>
      <EntryPointSymbol>main</EntryPointSymbol>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
    <ClCompile>
      <PrecompiledHeader>NotUsing</PrecompiledHeader>
      <PrecompiledHeaderFile>
      </PrecompiledHeaderFile>
      <WarningLevel>Level3</WarningLevel>
      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <AdditionalIncludeDirectories>..\..\..\dependencies\detours\include;..\..\..\dependencies\freeglut\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
      <LanguageStandard>stdcpp17</LanguageStandard>
    </ClCompile>
    <Link>
      <AdditionalDependencies>User32.lib</AdditionalDependencies>
      <SubSystem>Windows</SubSystem>
      <EntryPointSymbol>
      </EntryPointSymbol>
      <AdditionalLibraryDirectories>..\..\..\dependencies\detours\lib;..\..\..\dependencies\freeglut\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
    <ClCompile Include="dllmain.cpp" />
    <ClCompile Include="TabPadding.cpp" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Drawing" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="ConfigOption.h" />
    <ClInclude Include="framework.h" />
    <ClInclude Include="PluginConfig.h" />
    <ClInclude Include="PluginConfigApi.h" />
    <ClInclude Include="SkinnedMessageBox.h" />
    <ClInclude Include="TabPadding.h" />
    <ClInclude Include="ui.h">
      <FileType>CppForm</FileType>
    </ClInclude>
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="SkinnedMessageBox.resx" />
    <EmbeddedResource Include="ui.resx">
      <DependentUpon>ui.h</DependentUpon>
    </EmbeddedResource>
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
</Project>


code:
#pragma once
#include "ConfigOption.h"
#include "PluginConfigApi.h"

namespace PluginConfig
{
	ConfigOptionBase* GetConfigOption(PluginConfigOption cfg)
	{
		PluginConfigBooleanData* boolData = (PluginConfigBooleanData*)(cfg.data);
		PluginConfigNumericData* numericData = (PluginConfigNumericData*)(cfg.data);
		PluginConfigStringData* stringData = (PluginConfigStringData*)(cfg.data);
		PluginConfigDropdownIndexData* ddIdxData = (PluginConfigDropdownIndexData*)(cfg.data);
		PluginConfigDropdownTextData* ddTextData = (PluginConfigDropdownTextData*)(cfg.data);
		PluginConfigDropdownNumberData* ddNumberData = (PluginConfigDropdownNumberData*)(cfg.data);
		PluginConfigResolutionData* resData = (PluginConfigResolutionData*)(cfg.data);
		PluginConfigGroupData* groupData = (PluginConfigGroupData*)(cfg.data);
		PluginConfigButtonData* btnData = (PluginConfigButtonData*)(cfg.data);
		PluginConfigSpacerData* spacerData = (PluginConfigSpacerData*)(cfg.data);

		switch (cfg.cfgType)
		{
		case CONFIG_BOOLEAN:
			return new BooleanOption(boolData->iniVarName, boolData->iniSectionName, boolData->iniFilePath, boolData->friendlyName, boolData->description, boolData->defaultVal, boolData->saveAsString);
		case CONFIG_NUMERIC:
			return new NumericOption(numericData->iniVarName, numericData->iniSectionName, numericData->iniFilePath, numericData->friendlyName, numericData->description, numericData->defaultVal, numericData->minVal, numericData->maxVal);
		case CONFIG_STRING:
			return new StringOption(stringData->iniVarName, stringData->iniSectionName, stringData->iniFilePath, stringData->friendlyName, stringData->description, stringData->defaultVal, stringData->useUtf8);
		case CONFIG_DROPDOWN_INDEX:
			return new DropdownOption(ddIdxData->iniVarName, ddIdxData->iniSectionName, ddIdxData->iniFilePath, ddIdxData->friendlyName, ddIdxData->description, ddIdxData->defaultVal, ddIdxData->valueStrings);
		case CONFIG_DROPDOWN_TEXT:
			return new DropdownTextOption(ddTextData->iniVarName, ddTextData->iniSectionName, ddTextData->iniFilePath, ddTextData->friendlyName, ddTextData->description, ddTextData->defaultVal, ddTextData->valueStrings, ddTextData->editable, ddTextData->useUtf8);
		case CONFIG_DROPDOWN_NUMBER:
			return new DropdownNumberOption(ddNumberData->iniVarName, ddNumberData->iniSectionName, ddNumberData->iniFilePath, ddNumberData->friendlyName, ddNumberData->description, ddNumberData->defaultVal, ddNumberData->valueInts, ddNumberData->editable);
		case CONFIG_RESOLUTION:
			return new ResolutionOption(resData->iniVarName, resData->iniVarName2, resData->iniSectionName, resData->iniFilePath, resData->friendlyName, resData->description, resData->defaultVal, resData->valueResolutions, resData->editable);
		case CONFIG_GROUP_START:
			return new OptionMetaGroupStart(groupData->name, groupData->height);
		case CONFIG_GROUP_END:
			return new OptionMetaGroupEnd();
		case CONFIG_BUTTON:
			return new ButtonOption(btnData->friendlyName, btnData->description, btnData->func);
		case CONFIG_SPACER:
			return new OptionMetaSpacer(spacerData->height);
		default:
			return new ConfigOptionBase();
		}
	}

	std::vector<ConfigOptionBase*> GetConfigOptionVec(PluginConfigArray &in)
	{
		std::vector<ConfigOptionBase*> outvec;
		for (int i = 0; i < in.len; i++)
		{
			// basic check for validity
			if (in.options[i].data == nullptr)
				break;

			outvec.push_back(GetConfigOption(in.options[i]));
		}
		return outvec;
	}

	/*std::vector<PluginConfigOption> testcfg =
	{
		PluginConfigOption{ CONFIG_GROUP_START, new PluginConfigGroupData{L"Screen Resolution", 400 } },
		PluginConfigOption{ CONFIG_DROPDOWN_INDEX, new PluginConfigDropdownIndexData{ L"display", RESOLUTION_SECTION, CONFIG_FILE, L"Display:", L"Sets the window/screen mode.", 0, std::vector<LPCWSTR>({ L"Windowed", L"Borderless", L"Fullscreen" }) } },
		PluginConfigOption{ CONFIG_RESOLUTION, new PluginConfigResolutionData{ L"width", L"height", RESOLUTION_SECTION, CONFIG_FILE, L"Resolution:", L"Sets the display resolution.", resolution(1280, 720), getScreenResolutionsVec(screenModes) } }
	};*/

	//std::vector<ConfigOptionBase*> testcfgconfigopts = GetConfigOptionVec(testcfg);
};


code:
#pragma once
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

using namespace System;
using namespace System::Windows::Forms;
using namespace System::Drawing;

// ported from https://stackoverflow.com/a/7785745
// original code by user LarsTech https://stackoverflow.com/users/719186/larstech

static ref class SkinnedMessageBox
{
private: ref class SkinnedMessageBoxForm : Form
	{
	private:
		void SkinnedMessageBoxForm_Shown(Object^ sender, EventArgs^ e)
		{
			Activate();
		}

		void SkinnedMessageBoxForm_FormClosed(Object^ sender, FormClosedEventArgs^ e)
		{
			if (CancelButton && (DialogResult == System::Windows::Forms::DialogResult::None || DialogResult == System::Windows::Forms::DialogResult::Cancel))
				DialogResult = CancelButton->DialogResult;
		}

		void InitComponentNew()
		{
			this->SuspendLayout();

			this->AutoSize = true;
			this->FormBorderStyle = System::Windows::Forms::FormBorderStyle::FixedDialog;
			this->Width = 350;
			this->Height = 140;
			this->StartPosition = FormStartPosition::CenterScreen;
			this->MaximizeBox = false;
			this->MinimizeBox = false;
			this->ShowInTaskbar = false;
			this->ShowIcon = false;
			__try
			{
				this->Icon = Application::OpenForms[0]->Icon;
			}
			__except (1) { }
			this->Shown += gcnew EventHandler(this, &SkinnedMessageBoxForm::SkinnedMessageBoxForm_Shown);
			this->FormClosed += gcnew FormClosedEventHandler(this, &SkinnedMessageBoxForm::SkinnedMessageBoxForm_FormClosed);

			//this->Font = SystemFonts::MessageBoxFont;
			//this->Font = gcnew Font(SystemFonts::MessageBoxFont->FontFamily, SystemFonts::MessageBoxFont->SizeInPoints);
			this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::None;

			this->ResumeLayout(false);
			this->PerformLayout();
		}

		float ScaleWidth = 1.0;
		float ScaleHeight = 1.0;
		int btnWidth = 85;
		int btnHeight = 23;
		int btnPadding = 12;

		static cli::array<Button^>^ buttonrefs = gcnew cli::array<Button^>{ gcnew Button, gcnew Button, gcnew Button };
		int numbuttons = 1;

		Button^ AddBtn(String^ text, System::Windows::Forms::DialogResult result, int btnNumber)
		{
			Button^ btn = buttonrefs[btnNumber];
			btn->Text = text;
			btn->DialogResult = result;
			btn->Anchor = AnchorStyles::Bottom | AnchorStyles::Right;
			btn->Width = btnWidth;
			btn->Height = btnHeight;

			btn->Location = Point(ClientSize.Width - btnWidth * (numbuttons - btnNumber) - btnPadding * (numbuttons - btnNumber), ClientSize.Height - btnHeight - btnPadding);
			Controls->Add(btn);

			//buttonrefs[btnNumber] = btn;
			return btn;
		}

		void SetBackCol(Control^ elem, Color color)
		{
			Type^ elemType = elem->GetType();

			if ((elem->HasChildren || elemType == Button::typeid || elemType == ComboBox::typeid) && elemType != NumericUpDown::typeid)
			{
				elem->BackColor = color;

				for (int i = 0; i < elem->Controls->Count; i++)
				{
					SetBackCol(elem->Controls[i], color);
				}
			}
		}

		void SetForeCol(Control^ elem, Color color)
		{
			Type^ elemType = elem->GetType();

			if (elemType != NumericUpDown::typeid && elemType != TextBox::typeid)
			{
				elem->ForeColor = color;
			}
			if (elem->HasChildren && elemType != NumericUpDown::typeid)
			{
				for (int i = 0; i < elem->Controls->Count; i++)
				{
					SetForeCol(elem->Controls[i], color);
				}
			}
		}

		void SetUIStyle(Control^ elem, FlatStyle style)
		{
			Type^ elemType = elem->GetType();

			if (elemType == Button::typeid)
			{
				((Button^)elem)->FlatStyle = style;
			}
			else if (elemType  == ComboBox::typeid)
			{
				((ComboBox^)elem)->FlatStyle = style;
			}
			else if (elemType == CheckBox::typeid)
			{
				((CheckBox^)elem)->FlatStyle = style;
			}

			if (elem->HasChildren && elemType != NumericUpDown::typeid)
			{
				for (int i = 0; i < elem->Controls->Count; i++)
				{
					SetUIStyle(elem->Controls[i], style);
				}
			}
		}

	public:
		SkinnedMessageBoxForm(IWin32Window^ owner, String^ message, String^ caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultbutton, MessageBoxOptions options)
		{
			InitComponentNew();
			Graphics^ grfx = Graphics::FromHwnd(Handle);
			ScaleWidth = Font->SizeInPoints / 72 * grfx->DpiX / 10.67f;
			ScaleHeight = ScaleWidth;
			btnWidth *= ScaleWidth;
			btnHeight *= ScaleHeight;
			btnPadding *= ScaleWidth;

			Width *= ScaleWidth;
			Height *= ScaleHeight;

			if (owner->Handle != IntPtr::Zero)
			{
				Owner = Control::FromHandle(owner->Handle)->FindForm();
			}
			else
			{
				Owner = ActiveForm;
			}

			Text = caption;

			System::Windows::Forms::Padding lblMargin = System::Windows::Forms::Padding(14 * ScaleWidth, 0 * ScaleHeight, 14 * ScaleWidth, 45 * ScaleHeight);
			Point lblLocation = Point(14 * ScaleWidth, 14 * ScaleHeight);
			Drawing::Size lblMinSize = Drawing::Size(ClientSize.Width - lblLocation.X - lblMargin.Right, 0);
			Drawing::Size lblMaxSize = Drawing::Size(600 * ScaleWidth, 0 * ScaleHeight);

			Label^ MessageLbl = gcnew Label();
			MessageLbl->Text = message;
			MessageLbl->AutoSize = true;
			MessageLbl->Margin = lblMargin;
			MessageLbl->Location = lblLocation;
			MessageLbl->MinimumSize = lblMinSize;
			MessageLbl->MaximumSize = lblMaxSize;

			Controls->Add(MessageLbl);

			if (buttons == MessageBoxButtons::OK)
			{
				numbuttons = 1;
				Button^ OkBtn = AddBtn("&OK", System::Windows::Forms::DialogResult::OK, 0);
				CancelButton = OkBtn;
				//(not sure why, but this matches official .NET behaviour)
			}
			else if (buttons == MessageBoxButtons::OKCancel)
			{
				numbuttons = 2;
				AddBtn("&OK", System::Windows::Forms::DialogResult::OK, 0);
				Button^ CancelBtn = AddBtn("&Cancel", System::Windows::Forms::DialogResult::Cancel, 1);
				CancelButton = CancelBtn;
			}
			else if (buttons == MessageBoxButtons::YesNo)
			{
				numbuttons = 2;
				AddBtn("&Yes", System::Windows::Forms::DialogResult::Yes, 0);
				Button^ NoBtn = AddBtn("&No", System::Windows::Forms::DialogResult::No, 1);
				CancelButton = NoBtn;
				//(official .NET doesn't set cancel button, but this seems pretty clear to me...)
			}
			else if (buttons == MessageBoxButtons::RetryCancel)
			{
				numbuttons = 2;
				AddBtn("&Retry", System::Windows::Forms::DialogResult::Retry, 0);
				Button^ CancelBtn = AddBtn("&Cancel", System::Windows::Forms::DialogResult::Cancel, 1);
				CancelButton = CancelBtn;
			}
			else if (buttons == MessageBoxButtons::YesNoCancel)
			{
				numbuttons = 3;
				AddBtn("&Yes", System::Windows::Forms::DialogResult::Yes, 0);
				AddBtn("&No", System::Windows::Forms::DialogResult::No, 1);
				Button^ CancelBtn = AddBtn("&Cancel", System::Windows::Forms::DialogResult::Cancel, 2);
				CancelButton = CancelBtn;
			}
			else if (buttons == MessageBoxButtons::AbortRetryIgnore)
			{
				numbuttons = 3;
				AddBtn("&Abort", System::Windows::Forms::DialogResult::Abort, 0);
				AddBtn("&Retry", System::Windows::Forms::DialogResult::Retry, 1);
				AddBtn("&Ignore", System::Windows::Forms::DialogResult::Ignore, 2);
				//this.CancelButton = IgnoreBtn;
				//(official .NET doesn't set cancel button)
			}
			else
			{
				throw gcnew System::ComponentModel::InvalidEnumArgumentException("buttons", (int)buttons, MessageBoxButtons::typeid);
			}

			if (!CancelButton)
				ControlBox = false;

			if (icon != MessageBoxIcon::None)
			{
				int iconsize = 32 * ScaleWidth;
				int iconpadding = 12 * ScaleWidth;
				int textpadding = -8 * ScaleWidth;

				PictureBox^ iconPB = gcnew PictureBox();
				iconPB->Width = iconsize;
				iconPB->Height = iconsize;
				iconPB->Top = 10 * ScaleHeight;
				iconPB->Left = 10 * ScaleWidth;
				iconPB->AccessibleName = "Icon";
				iconPB->SizeMode = PictureBoxSizeMode::Zoom;

				System::Resources::ResourceManager^ ResMgr = gcnew System::Resources::ResourceManager("Launcher.SkinnedMessageBox", SkinnedMessageBox::typeid->Assembly);

				// there are many duplicated values:
				// https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.messageboxicon?view=netframework-4.7.2
				if (icon == MessageBoxIcon::Error) // also Hand and Stop
				{
					Drawing::Icon^ erricon = gcnew Drawing::Icon(SystemIcons::Error, iconsize, iconsize);
					iconPB->Image = erricon->ToBitmap();
					iconPB->AccessibleDescription = "Error";
					//this.Icon = SystemIcons.Error;
					System::Media::SystemSounds::Hand->Play();
				}
				else if (icon == MessageBoxIcon::Question)
				{
					iconPB->Image = (Image^)(ResMgr->GetObject("PokeWhat4xvc2"));
					iconPB->AccessibleDescription = "Question";
					//this.Icon = SystemIcons.Question;
					System::Media::SystemSounds::Question->Play();
				}
				else if (icon == MessageBoxIcon::Warning) // also Exclamation
				{
					iconPB->Image = (Image^)(ResMgr->GetObject("PokeSnarf4xd"));
					iconPB->AccessibleDescription = "Warning";
					//this.Icon = SystemIcons.Warning;
					//textpadding -= 2 * ScaleWidth; // triangle shape looks worse without this
					System::Media::SystemSounds::Exclamation->Play();
				}
				else if (icon == MessageBoxIcon::Information) // also Asterisk
				{
					Drawing::Icon^ infoicon = gcnew Drawing::Icon(SystemIcons::Information, iconsize, iconsize);
					iconPB->Image = infoicon->ToBitmap();
					iconPB->AccessibleDescription = "Information";
					//this.Icon = SystemIcons.Question;
					System::Media::SystemSounds::Asterisk->Play();
				}
				else
				{
					throw gcnew System::ComponentModel::InvalidEnumArgumentException("icon", (int)icon, MessageBoxIcon::typeid);
				}

				MessageLbl->MinimumSize = Drawing::Size(MessageLbl->MinimumSize.Width - (iconsize + iconpadding + textpadding), MessageLbl->MinimumSize.Height);
				MessageLbl->Left += (iconsize + iconpadding + textpadding);
				Controls->Add(iconPB);
			}

			if (defaultbutton == MessageBoxDefaultButton::Button1)
			{
				AcceptButton = buttonrefs[0];
				ActiveControl = buttonrefs[0];
			}
			else if (defaultbutton == MessageBoxDefaultButton::Button2)
			{
				if (!Controls->Contains(buttonrefs[1])) throw gcnew ArgumentOutOfRangeException("defaultbutton", "defaultbutton cannot be set to a button number that does not exist");
				AcceptButton = buttonrefs[1];
				ActiveControl = buttonrefs[1];
			}
			else if (defaultbutton == MessageBoxDefaultButton::Button3)
			{
				if (!Controls->Contains(buttonrefs[2])) throw gcnew ArgumentOutOfRangeException("defaultbutton", "defaultbutton cannot be set to a button number that does not exist");
				AcceptButton = buttonrefs[2];
				ActiveControl = buttonrefs[2];
			}
			else
			{
				throw gcnew System::ComponentModel::InvalidEnumArgumentException("defaultbutton", (int)defaultbutton, MessageBoxDefaultButton::typeid);
			}

			// TODO: TEXT ALIGNMENT AND RTL MODE
			/*
				// MessageBoxOptions are more like flags than concrete settings.
				// For ease of use, we assume default value is whatever winforms gives us in current locale -- but we override if any value is set
				if ((options & MessageBoxOptions.RightAlign) == MessageBoxOptions.RightAlign) MessageLbl.TextAlign = ContentAlignment.TopRight;
				else if (options != 0) MessageLbl.TextAlign = ContentAlignment.TopLeft;

				// RtlReading should be what actually affects layout, and RightAlign only affects the text label
				if ((options & MessageBoxOptions.RtlReading) == MessageBoxOptions.RtlReading)
				{
				    foreach (Control control in this.Controls)
				    {
				        control.Left = this.ClientSize.Width - control.Right;
				        control.RightToLeft = RightToLeft.Yes;
				    }
				    this.RightToLeft = RightToLeft.Yes;
				    this.RightToLeftLayout = true;
				}
				else if (options != 0)
				{
				    foreach (Control control in this.Controls)
				    {
				        control.RightToLeft = RightToLeft.No;
				    }
				    this.RightToLeft = RightToLeft.No;
				    this.RightToLeftLayout = false;
				}
			*/

			SetBackCol(this, Color::FromArgb(64, 64, 64));
			SetForeCol(this, Color::White);
			SetUIStyle(this, FlatStyle::Flat);
		}

		SkinnedMessageBoxForm(String^ message, String^ caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultbutton, MessageBoxOptions options)
		{
			SkinnedMessageBoxForm(NativeWindow::FromHandle((IntPtr::Zero)), message, caption, buttons, icon, defaultbutton, options);
		}
	};

public:
	static DialogResult Show(IWin32Window^ owner, String^ message, String^ caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultbutton, MessageBoxOptions options)
	{
		// I probably shouldn't do this, but from what I can tell this should be safe enough -- out of thread owner is allowed
		// and it only seems to appear when debugging (???)
		Control::CheckForIllegalCrossThreadCalls = false;
		SkinnedMessageBoxForm^ mb = gcnew SkinnedMessageBoxForm(owner, message, caption, buttons, icon, defaultbutton, options);
		DialogResult res = mb->ShowDialog();
		Control::CheckForIllegalCrossThreadCalls = true;
		return res;
	}

	static DialogResult Show(IWin32Window^ owner, String^ message, String^ caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultbutton)
	{
		return Show(owner, message, caption, buttons, icon, defaultbutton, (MessageBoxOptions)0);
	}

	static DialogResult Show(IWin32Window^ owner, String^ message, String^ caption, MessageBoxButtons buttons, MessageBoxIcon icon)
	{
		return Show(owner, message, caption, buttons, icon, MessageBoxDefaultButton::Button1, (MessageBoxOptions)0);
	}

	static DialogResult Show(IWin32Window^ owner, String^ message, String^ caption, MessageBoxButtons buttons)
	{
		return Show(owner, message, caption, buttons, MessageBoxIcon::None, MessageBoxDefaultButton::Button1, (MessageBoxOptions)0);
	}

	static DialogResult Show(IWin32Window^ owner, String^ message, String^ caption)
	{
		return Show(owner, message, caption, MessageBoxButtons::OK, MessageBoxIcon::None, MessageBoxDefaultButton::Button1, (MessageBoxOptions)0);
	}

	static DialogResult Show(IWin32Window^ owner, String^ message)
	{
		return Show(owner, message, "", MessageBoxButtons::OK, MessageBoxIcon::None, MessageBoxDefaultButton::Button1, (MessageBoxOptions)0);
	}
};


code:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema

    Version 2.0

    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.

    Example:

    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>

    There are any number of "resheader" rows that contain simple
    name/value pairs.

    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.

    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:

    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.

    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  <data name="PokeSnarf4xd" type="System.Resources.ResXFileRef, System.Windows.Forms">
    <value>PokeSnarf4xd.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
  </data>
  <data name="PokeWhat4xvc2" type="System.Resources.ResXFileRef, System.Windows.Forms">
    <value>PokeWhat4xvc2.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
  </data>
</root>


code:
#include "TabPadding.h"
//#include <Windows.h>

// ported from https://stackoverflow.com/a/7785745
// original code by user LarsTech https://stackoverflow.com/users/719186/larstech

TabPadding::TabPadding(TabControl^ tc)
{
	tabControl = tc;
	tabControl->Selected += gcnew TabControlEventHandler(this, &TabPadding::tabControl_Selected);
	AssignHandle(tc->Handle);
}

void TabPadding::tabControl_Selected(Object^ sender, TabControlEventArgs^ e)
{
	tabControl->Invalidate();
}

void TabPadding::WndProc(Message %m) {
	NativeWindow::WndProc(m);

	if (m.Msg == 15) { //WM_PAINT
		Graphics^ g = Graphics::FromHwnd(m.HWnd);

		//Replace the outside white borders:
		if (tabControl->Parent) {
			g->SetClip(System::Drawing::Rectangle(0, 0, tabControl->Width - 2, tabControl->Height - 1), Drawing2D::CombineMode::Exclude);
			g->FillRectangle(gcnew SolidBrush(tabControl->Parent->BackColor),
				System::Drawing::Rectangle(0, tabControl->ItemSize.Height + 2, tabControl->Width, tabControl->Height - (tabControl->ItemSize.Height + 2)));
		}

		//Replace the inside white borders:
		if (tabControl->SelectedTab) {
			g->ResetClip();
			System::Drawing::Rectangle r = tabControl->SelectedTab->Bounds;
			g->SetClip(r, Drawing2D::CombineMode::Exclude);
			g->FillRectangle(gcnew SolidBrush(tabControl->SelectedTab->BackColor),
				System::Drawing::Rectangle(r.Left - 3, r.Top - 1, r.Width + 4, r.Height + 3));
		}

		delete g;
	}
}


code:
#pragma once
using namespace System::Windows::Forms;
using namespace System::Drawing;

// ported from https://stackoverflow.com/a/7785745
// original code by user LarsTech https://stackoverflow.com/users/719186/larstech

ref class TabPadding : public NativeWindow
{
public:
	TabPadding(TabControl^ tc);

private:
	TabControl^ tabControl;
	void tabControl_Selected(Object^ sender, TabControlEventArgs^ e);

protected:
	virtual void WndProc(Message %m) override;
};


code:
#include "ui.h"
#include "framework.h"
#include <detours.h>
#pragma comment(lib, "detours.lib")

using namespace System;
using namespace System::Windows::Forms;

[STAThread]
int showUI() {
	SetProcessDPIAware();
	Application::EnableVisualStyles();
	Application::SetCompatibleTextRenderingDefault(false);
	Application::Run(gcnew Launcher::ui());
	return 0;
}

int hookedMain(int argc, const char** argv, const char** envp)
{
	for (int i = 0; i < argc; ++i)
	{
		arg = argv[i];
		if (arg == "--launch" || nSkipLauncher)
			return divaMain(argc, argv, envp);
	}
	return showUI();
}

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	if (ul_reason_for_call == DLL_PROCESS_ATTACH)
	{
		DisableThreadLibraryCalls(hModule);
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		DetourAttach(&(PVOID&)divaMain, hookedMain);
		DetourTransactionCommit();
	}
	return TRUE;
}