#include "mungal4.h"
#pragma hdrstop

//########################################################################
// L4JOY - the generic-joystick reader. Design and device model in
// L4JOY.h. Consumed by the PadRIO poll through the joydev / joyaxis /
// joybutton / joyhat bindings (L4PADBINDINGS / L4PADRIO).
//########################################################################

#include "l4joy.h"

#define DIRECTINPUT_VERSION 0x0800
#include <windows.h>
#include <dinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//########################################################################
// State
//########################################################################

namespace
{
	struct JoyDevice
	{
		IDirectInputDevice8A	*device;
		RPJoyDeviceState		state;
		//
		// Per-axis calibrated range. DIPROP_RANGE is set to +-32767 when
		// the device is opened, but a driver may refuse, so normalization
		// uses what the device actually reports.
		//
		LONG					axisMin[joyAxisCount];
		LONG					axisMax[joyAxisCount];
	};

	IDirectInput8A	*gDirectInput = NULL;
	JoyDevice		gDevices[joyMaxDevices];
	int				gDeviceCount = 0;
	int				gInitialized = 0;
	unsigned long	gLastProbeTick = 0;

	int JoyLogEnabled()
	{
		static int log = -1;
		if (log < 0)
		{
			const char *value = getenv("RP412JOYLOG");
			log = (value != NULL && *value != '0') ? 1 : 0;
		}
		return log;
	}

	//###################################################################
	// XInput-device exclusion.
	//
	// The documented WMI-free method: every XInput-capable device's
	// RawInput device path carries the "IG_" marker. Collect the VID/PID
	// of each such path once per enumeration pass and skip any
	// DirectInput device whose guidProduct matches - DI packs VID in the
	// low word and PID in the high word of guidProduct.Data1.
	//
	// Without this an Xbox pad arrives through both APIs and every
	// button counts twice.
	//###################################################################

	enum { xinputVidPidMax = 16 };
	unsigned long	gXInputVidPid[xinputVidPidMax];
	int				gXInputVidPidCount = 0;

	void CollectXInputVidPids()
	{
		gXInputVidPidCount = 0;

		UINT device_count = 0;
		if (GetRawInputDeviceList(NULL, &device_count,
				sizeof(RAWINPUTDEVICELIST)) != 0 || device_count == 0)
		{
			return;
		}
		RAWINPUTDEVICELIST *list = (RAWINPUTDEVICELIST *)
			malloc(device_count * sizeof(RAWINPUTDEVICELIST));
		if (list == NULL)
		{
			return;
		}
		device_count = GetRawInputDeviceList(list, &device_count,
			sizeof(RAWINPUTDEVICELIST));
		if (device_count == (UINT) -1)
		{
			free(list);
			return;
		}

		for (UINT i = 0; i < device_count; ++i)
		{
			if (list[i].dwType != RIM_TYPEHID)
			{
				continue;
			}
			char path[256];
			UINT size = sizeof(path);
			if (GetRawInputDeviceInfoA(list[i].hDevice, RIDI_DEVICENAME,
					path, &size) == (UINT) -1)
			{
				continue;
			}
			path[sizeof(path) - 1] = '\0';
			if (strstr(path, "IG_") == NULL && strstr(path, "ig_") == NULL)
			{
				continue;
			}
			//
			// Parse "...VID_045E&PID_028E..." - case varies by driver.
			//
			const char *v = strstr(path, "VID_");
			if (v == NULL)	v = strstr(path, "vid_");
			const char *p = strstr(path, "PID_");
			if (p == NULL)	p = strstr(path, "pid_");
			if (v == NULL || p == NULL)
			{
				continue;
			}
			unsigned vid = (unsigned) strtoul(v + 4, NULL, 16);
			unsigned pid = (unsigned) strtoul(p + 4, NULL, 16);
			if (gXInputVidPidCount < xinputVidPidMax)
			{
				gXInputVidPid[gXInputVidPidCount++] =
					((unsigned long) pid << 16) | vid;
			}
		}
		free(list);
	}

	int IsXInputProduct(const GUID &guid_product)
	{
		for (int i = 0; i < gXInputVidPidCount; ++i)
		{
			if (gXInputVidPid[i] == (unsigned long) guid_product.Data1)
			{
				return 1;
			}
		}
		return 0;
	}

	//###################################################################
	// Device open
	//###################################################################

	//
	// A window handle of THIS process for SetCooperativeLevel. BACKGROUND
	// + NONEXCLUSIVE matches the XInput model: the device stays readable
	// while the game runs, and PadRIO's own rules remain the arbiter of
	// what acts on it.
	//
	BOOL CALLBACK FindProcessWindowCallback(HWND hwnd, LPARAM lparam)
	{
		DWORD process_id = 0;
		GetWindowThreadProcessId(hwnd, &process_id);
		if (process_id == GetCurrentProcessId())
		{
			*(HWND *) lparam = hwnd;
			return FALSE;
		}
		return TRUE;
	}

	HWND FindProcessWindow()
	{
		HWND hwnd = NULL;
		EnumWindows(FindProcessWindowCallback, (LPARAM) &hwnd);
		return (hwnd != NULL) ? hwnd : GetDesktopWindow();
	}

	BOOL CALLBACK SetAxisRangeCallback(
		LPCDIDEVICEOBJECTINSTANCEA object, LPVOID context)
	{
		IDirectInputDevice8A *device = (IDirectInputDevice8A *) context;

		DIPROPRANGE range;
		range.diph.dwSize = sizeof(DIPROPRANGE);
		range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
		range.diph.dwHow = DIPH_BYID;
		range.diph.dwObj = object->dwType;
		range.lMin = -32767;
		range.lMax = 32767;
		device->SetProperty(DIPROP_RANGE, &range.diph);		// best effort
		return DIENUM_CONTINUE;
	}

	BOOL CALLBACK EnumDevicesCallback(
		const DIDEVICEINSTANCEA *instance, VOID *context)
	{
		HWND owner = *(HWND *) context;

		if (gDeviceCount >= joyMaxDevices)
		{
			return DIENUM_STOP;
		}
		if (IsXInputProduct(instance->guidProduct))
		{
			if (JoyLogEnabled())
			{
				DEBUG_STREAM << "Joy: skipping XInput-class device \""
					<< instance->tszProductName << "\"\n" << std::flush;
			}
			return DIENUM_CONTINUE;
		}

		IDirectInputDevice8A *device = NULL;
		if (FAILED(gDirectInput->CreateDevice(instance->guidInstance,
				&device, NULL)) || device == NULL)
		{
			return DIENUM_CONTINUE;
		}
		if (FAILED(device->SetDataFormat(&c_dfDIJoystick2)))
		{
			device->Release();
			return DIENUM_CONTINUE;
		}
		//
		// Best effort: some environments reject a cooperative level on
		// the desktop window, and the default still polls.
		//
		device->SetCooperativeLevel(owner,
			DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
		device->EnumObjects(SetAxisRangeCallback, device, DIDFT_AXIS);
		device->Acquire();

		JoyDevice &slot = gDevices[gDeviceCount];
		memset(&slot, 0, sizeof(slot));
		slot.device = device;
		slot.state.attached = 1;
		strncpy(slot.state.name, instance->tszProductName,
			sizeof(slot.state.name) - 1);
		for (int a = 0; a < joyAxisCount; ++a)
		{
			slot.axisMin[a] = -32767;
			slot.axisMax[a] = 32767;
		}
		for (int h = 0; h < joyHatCount; ++h)
		{
			slot.state.hat[h] = -1;
		}
		++gDeviceCount;

		DEBUG_STREAM << "Joy: device " << (gDeviceCount - 1) << ": \""
			<< slot.state.name << "\" attached\n" << std::flush;
		return DIENUM_CONTINUE;
	}

	void ReleaseAllDevices()
	{
		for (int i = 0; i < gDeviceCount; ++i)
		{
			if (gDevices[i].device != NULL)
			{
				gDevices[i].device->Unacquire();
				gDevices[i].device->Release();
				gDevices[i].device = NULL;
			}
			gDevices[i].state.attached = 0;
		}
		gDeviceCount = 0;
	}

	void Enumerate()
	{
		ReleaseAllDevices();
		CollectXInputVidPids();

		HWND owner = FindProcessWindow();
		gDirectInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumDevicesCallback,
			&owner, DIEDFL_ATTACHEDONLY);
	}

	float NormalizeAxis(LONG value, LONG range_min, LONG range_max)
	{
		if (range_max <= range_min)
		{
			return 0.0f;
		}
		float normalized =
			((float)(value - range_min) / (float)(range_max - range_min))
				* 2.0f - 1.0f;
		if (normalized < -1.0f)	normalized = -1.0f;
		if (normalized > 1.0f)	normalized = 1.0f;
		return normalized;
	}
}

//########################################################################
// Public surface
//########################################################################

int
	RPJoyInit(void)
{
	if (gInitialized)
	{
		return gDeviceCount;
	}
	gInitialized = 1;

	if (FAILED(DirectInput8Create(GetModuleHandleA(NULL), DIRECTINPUT_VERSION,
			IID_IDirectInput8A, (void **) &gDirectInput, NULL))
		|| gDirectInput == NULL)
	{
		gDirectInput = NULL;
		DEBUG_STREAM << "Joy: DirectInput8Create failed - generic joysticks "
			<< "unavailable\n" << std::flush;
		return 0;
	}
	Enumerate();
	return gDeviceCount;
}

void
	RPJoyShutdown(void)
{
	ReleaseAllDevices();
	if (gDirectInput != NULL)
	{
		gDirectInput->Release();
		gDirectInput = NULL;
	}
	gInitialized = 0;
}

void
	RPJoyPoll(void)
{
	if (!gInitialized)
	{
		RPJoyInit();
	}
	if (gDirectInput == NULL)
	{
		return;
	}

	//
	// Hot-plug: when nothing is attached, re-enumerate on the same ~3 s
	// cadence PadRIO uses to look for a pad. Enumeration is far too heavy
	// to run every frame.
	//
	int any_attached = 0;
	for (int i = 0; i < gDeviceCount; ++i)
	{
		if (gDevices[i].state.attached)
		{
			any_attached = 1;
			break;
		}
	}
	if (!any_attached)
	{
		unsigned long now = GetTickCount();
		if (gLastProbeTick != 0 && now - gLastProbeTick < 3000)
		{
			return;
		}
		gLastProbeTick = now;
		Enumerate();
	}

	for (int i = 0; i < gDeviceCount; ++i)
	{
		JoyDevice &slot = gDevices[i];
		if (slot.device == NULL || !slot.state.attached)
		{
			continue;
		}

		HRESULT result = slot.device->Poll();
		if (FAILED(result))
		{
			result = slot.device->Acquire();
			if (SUCCEEDED(result))
			{
				result = slot.device->Poll();
			}
		}
		DIJOYSTATE2 joystate;
		if (SUCCEEDED(result) || result == DI_NOEFFECT)
		{
			result = slot.device->GetDeviceState(sizeof(joystate), &joystate);
		}
		if (FAILED(result))
		{
			//
			// Unplugged, or the driver died. Mark it detached and zero
			// the state: the binding layer then sees everything released
			// rather than holding whatever was pressed at the moment the
			// device went away.
			//
			DEBUG_STREAM << "Joy: device " << i << " (\"" << slot.state.name
				<< "\") lost\n" << std::flush;
			slot.state.attached = 0;
			memset(slot.state.axis, 0, sizeof(slot.state.axis));
			slot.state.buttons = 0;
			for (int h = 0; h < joyHatCount; ++h)
			{
				slot.state.hat[h] = -1;
			}
			continue;
		}

		LONG raw[joyAxisCount];
		raw[0] = joystate.lX;
		raw[1] = joystate.lY;
		raw[2] = joystate.lZ;
		raw[3] = joystate.lRx;
		raw[4] = joystate.lRy;
		raw[5] = joystate.lRz;
		raw[6] = joystate.rglSlider[0];
		raw[7] = joystate.rglSlider[1];
		for (int a = 0; a < joyAxisCount; ++a)
		{
			slot.state.axis[a] =
				NormalizeAxis(raw[a], slot.axisMin[a], slot.axisMax[a]);
		}

		slot.state.buttons = 0;
		for (int b = 0; b < joyButtonCount; ++b)
		{
			if (joystate.rgbButtons[b] & 0x80)
			{
				slot.state.buttons |= (1u << b);
			}
		}

		for (int h = 0; h < joyHatCount; ++h)
		{
			DWORD pov = joystate.rgdwPOV[h];
			//
			// Centered reads as 0xFFFF in the low word per the DI
			// contract; some drivers return the full 0xFFFFFFFF.
			//
			slot.state.hat[h] = (LOWORD(pov) == 0xFFFF) ? -1 : (int) pov;
		}
	}
}

int
	RPJoyDeviceCount(void)
{
	return gDeviceCount;
}

const RPJoyDeviceState *
	RPJoyDevice(int index)
{
	if (index < 0 || index >= gDeviceCount || !gDevices[index].state.attached)
	{
		return NULL;
	}
	return &gDevices[index].state;
}

int
	RPJoyFindDevice(const char *name_substring)
{
	if (name_substring == NULL || *name_substring == '\0')
	{
		return -1;
	}
	char want[64];
	strncpy(want, name_substring, sizeof(want) - 1);
	want[sizeof(want) - 1] = '\0';
	_strlwr(want);

	for (int i = 0; i < gDeviceCount; ++i)
	{
		if (!gDevices[i].state.attached)
		{
			continue;
		}
		char have[64];
		strncpy(have, gDevices[i].state.name, sizeof(have) - 1);
		have[sizeof(have) - 1] = '\0';
		_strlwr(have);
		if (strstr(have, want) != NULL)
		{
			return i;
		}
	}
	return -1;
}

//########################################################################
//########################### RP412JOYCONFIG #############################
//########################################################################
//
// The interactive capture wizard.
//
// Console UI - the game is a GUI app, so it allocates one. It detects
// which device and axis the player moves for each pod control, derives
// the sign convention from the DIRECTION of the move, then writes the
// joystick section of bindings.txt between marker lines. Everything
// outside the markers is preserved byte for byte, so a player's own
// keyboard and pad edits survive re-running it.
//
// Deriving the sign from the move is the point: a stick that reads
// positive when pushed right and one that reads negative are equally
// common, and no amount of documentation gets a player to work out which
// they own.
//
//########################################################################

#include <conio.h>
#include "l4padbindings.h"

namespace
{
	struct WizardCapture
	{
		int		used;
		int		device;
		int		axis;		// axis index, -1 for buttons
		int		button;		// button index, -1 for axes
		int		invert;
		char	line[128];
	};

	const char *JoyAxisToken(int axis)
	{
		static const char *names[joyAxisCount] =
			{ "X", "Y", "Z", "RX", "RY", "RZ", "SL0", "SL1" };
		return (axis >= 0 && axis < joyAxisCount) ? names[axis] : "?";
	}

	void WizardBaseline(float baseline[joyMaxDevices][joyAxisCount])
	{
		//
		// ~600 ms of samples gives the at-rest position of every axis. A
		// HOTAS throttle rests wherever its lever was left, so an
		// assumed zero would read as a huge deflection.
		//
		for (int pass = 0; pass < 20; ++pass)
		{
			RPJoyPoll();
			Sleep(30);
		}
		for (int d = 0; d < joyMaxDevices; ++d)
		{
			const RPJoyDeviceState *state = RPJoyDevice(d);
			for (int a = 0; a < joyAxisCount; ++a)
			{
				baseline[d][a] = (state != NULL) ? state->axis[a] : 0.0f;
			}
		}
	}

	//
	// Wait for a decisive axis move. Returns 1 with the outputs filled,
	// 0 on skip (SPACE), -1 on abort (ESC) or timeout.
	//
	int WizardCaptureAxis(
		const float baseline[joyMaxDevices][joyAxisCount],
		const WizardCapture *taken, int taken_count,
		int allow_skip,
		int *out_device, int *out_axis, float *out_delta, float *out_final)
	{
		unsigned long deadline = GetTickCount() + 30000;
		unsigned long hold_since = 0;
		int candidate_device = -1, candidate_axis = -1;

		while (GetTickCount() < deadline)
		{
			while (_kbhit())
			{
				int key = _getch();
				if (key == 27)
				{
					return -1;
				}
				if (key == ' ' && allow_skip)
				{
					return 0;
				}
			}
			RPJoyPoll();

			int best_device = -1, best_axis = -1;
			float best_magnitude = 0.0f, best_delta = 0.0f;
			for (int d = 0; d < joyMaxDevices; ++d)
			{
				const RPJoyDeviceState *state = RPJoyDevice(d);
				if (state == NULL)
				{
					continue;
				}
				for (int a = 0; a < joyAxisCount; ++a)
				{
					//
					// An axis already claimed cannot be claimed again -
					// otherwise one twitchy axis wins every prompt.
					//
					int already = 0;
					for (int t = 0; t < taken_count; ++t)
					{
						if (taken[t].used && taken[t].axis >= 0 &&
							taken[t].device == d && taken[t].axis == a)
						{
							already = 1;
						}
					}
					if (already)
					{
						continue;
					}
					float delta = state->axis[a] - baseline[d][a];
					float magnitude = (delta < 0.0f) ? -delta : delta;
					if (magnitude > best_magnitude)
					{
						best_magnitude = magnitude;
						best_delta = delta;
						best_device = d;
						best_axis = a;
					}
				}
			}

			//
			// Nearly half of full travel, held for a quarter second: big
			// enough that a resting hand or a noisy pot cannot trip it,
			// and the hold means a knocked stick passing through does
			// not either.
			//
			if (best_magnitude > 0.45f)
			{
				if (candidate_device != best_device || candidate_axis != best_axis)
				{
					candidate_device = best_device;
					candidate_axis = best_axis;
					hold_since = GetTickCount();
				}
				else if (GetTickCount() - hold_since > 250)
				{
					const RPJoyDeviceState *state = RPJoyDevice(best_device);
					*out_device = best_device;
					*out_axis = best_axis;
					*out_delta = best_delta;
					*out_final = (state != NULL)
						? state->axis[best_axis] : best_delta;
					return 1;
				}
			}
			else
			{
				candidate_device = candidate_axis = -1;
			}
			Sleep(15);
		}
		return -1;
	}

	//
	// Wait for a fresh button press. Returns 1 with the outputs filled,
	// 0 on skip, -1 on abort or timeout.
	//
	int WizardCaptureButton(int allow_skip, int *out_device, int *out_button)
	{
		//
		// Baseline the buttons already held, so a trigger squeezed since
		// the last prompt does not answer this one by itself.
		//
		unsigned baseline[joyMaxDevices];
		RPJoyPoll();
		for (int d = 0; d < joyMaxDevices; ++d)
		{
			const RPJoyDeviceState *state = RPJoyDevice(d);
			baseline[d] = (state != NULL) ? state->buttons : 0;
		}

		unsigned long deadline = GetTickCount() + 30000;
		while (GetTickCount() < deadline)
		{
			while (_kbhit())
			{
				int key = _getch();
				if (key == 27)
				{
					return -1;
				}
				if (key == ' ' && allow_skip)
				{
					return 0;
				}
			}
			RPJoyPoll();
			for (int d = 0; d < joyMaxDevices; ++d)
			{
				const RPJoyDeviceState *state = RPJoyDevice(d);
				if (state == NULL)
				{
					continue;
				}
				unsigned fresh = state->buttons & ~baseline[d];
				if (fresh != 0)
				{
					int button = 0;
					while ((fresh & 1u) == 0)
					{
						fresh >>= 1;
						++button;
					}
					*out_device = d;
					*out_button = button;
					return 1;
				}
				baseline[d] &= state->buttons;	// releases refresh it
			}
			Sleep(15);
		}
		return -1;
	}
}

int
	RPJoyConfigWizard(void)
{
	//
	// The game is a GUI-subsystem app and has no console of its own.
	//
	if (GetConsoleWindow() == NULL)
	{
		AllocConsole();
	}
	FILE *io;
	freopen_s(&io, "CONOUT$", "w", stdout);
	freopen_s(&io, "CONIN$", "r", stdin);

	printf("\n=== Red Planet joystick setup (RP412JOYCONFIG) ===\n\n");

	//
	// Make sure bindings.txt exists before we start: the wizard only
	// writes its own section, and the keyboard and pad rows come from
	// the default write.
	//
	{
		PadBindingProfile ensure_default;
		PadBindings_Load(&ensure_default);
	}

	if (RPJoyInit() == 0)
	{
		printf("No generic (non-Xbox) game devices found.\n");
		printf("Plug in the stick, throttle or pedals and run joyconfig again.\n");
		printf("(Xbox-class controllers already work - no setup needed.)\n\n");
		printf("Press any key to exit.\n");
		_getch();
		return 1;
	}

	//
	// Let the devices settle and report where every axis is sitting.
	// Worth printing rather than assuming: a driver that refuses the
	// +-32767 range we ask for reports its own, and an axis then rests
	// hard over instead of near zero. Seeing "X +1.00" on an untouched
	// stick is the difference between a five-minute fix and a bug report
	// that says "it configured itself".
	//
	{
		float settle[joyMaxDevices][joyAxisCount];
		WizardBaseline(settle);

		printf("Detected devices (axes at rest):\n");
		for (int d = 0; d < RPJoyDeviceCount(); ++d)
		{
			const RPJoyDeviceState *state = RPJoyDevice(d);
			if (state == NULL)
			{
				continue;
			}
			printf("  [%d] %s\n      ", d, state->name);
			for (int a = 0; a < joyAxisCount; ++a)
			{
				printf("%s %+.2f  ", JoyAxisToken(a), settle[d][a]);
			}
			printf("\n");
		}
	}
	printf("\nFor each prompt, MOVE the control you want, or press SPACE to\n"
		"skip it, ESC to abort. Keep everything else still.\n\n");

	WizardCapture captures[16];
	memset(captures, 0, sizeof(captures));
	int capture_count = 0;

	//
	// The pod's analog channels. wants_negative says the asked-for move
	// should read NEGATIVE in the pod's sign convention, which is what
	// decides whether the captured axis gets an invert:
	//
	//   JoystickX   left +1, right -1
	//   JoystickY   forward -1, back +1
	//   Pedals      right +1, left -1 (the composite that decomposes
	//               into the pod's two pedals)
	//
	struct AxisStep
	{
		const char	*prompt;
		const char	*channel;
		int			wants_negative;
		int			lever;			// full-travel lever: sign from where
									// it ENDS, not which way it moved
		int			allow_skip;
	};
	static const AxisStep axisSteps[] =
	{
		{ "STEER: push the STICK / turn the WHEEL fully RIGHT",
		                                                "JoystickX", 1, 0, 0 },
		{ "PITCH: push the STICK fully FORWARD\n"
		  "       (add or remove the word invert on that line in\n"
		  "        bindings.txt to flip it later)",
		                                                "JoystickY", 1, 0, 0 },
		{ "PEDALS: twist the stick / press the RIGHT rudder pedal\n"
		  "        (SPACE if you have neither)",
		                                                "Pedals",    0, 0, 1 },
		{ "THROTTLE: move the throttle lever to FULL (SPACE if none)",
		                                                "Throttle",  0, 1, 1 }
	};

	float baseline[joyMaxDevices][joyAxisCount];

	for (int s = 0; s < (int)(sizeof(axisSteps) / sizeof(axisSteps[0])); ++s)
	{
		printf("%s ...\n", axisSteps[s].prompt);
		WizardBaseline(baseline);
		int device, axis;
		float delta, final_value;
		int got = WizardCaptureAxis(baseline, captures, capture_count,
			axisSteps[s].allow_skip, &device, &axis, &delta, &final_value);
		if (got < 0)
		{
			printf("\nAborted - nothing written.\n");
			printf("Press any key to continue into the game.\n");
			_getch();
			return 1;
		}
		if (got == 0)
		{
			printf("  skipped.\n\n");
			continue;
		}
		WizardCapture &capture = captures[capture_count++];
		capture.used = 1;
		capture.device = device;
		capture.axis = axis;
		capture.button = -1;

		if (axisSteps[s].lever)
		{
			//
			// A lever has no rest position to move away from, so the
			// sign comes from where it finished: full-forward reading
			// negative means the axis runs backwards for us.
			//
			capture.invert = (final_value < 0.0f);
			sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0",
				JoyAxisToken(axis), axisSteps[s].channel,
				capture.invert ? " invert" : "");
		}
		else
		{
			int went_negative = (delta < 0.0f);
			capture.invert = axisSteps[s].wants_negative
				? !went_negative : went_negative;
			sprintf(capture.line, "joyaxis %s axis %s%s deadzone 0.08",
				JoyAxisToken(axis), axisSteps[s].channel,
				capture.invert ? " invert" : "");
		}
		//
		// The move is reported, not just the axis: a capture nobody made
		// shows up here as a small delta, and a player who wonders why
		// the wrong control answered can see what the wizard saw.
		//
		printf("  -> device %d (%s) axis %s%s   [moved %+.2f, now %+.2f]\n\n",
			device,
			(RPJoyDevice(device) != NULL) ? RPJoyDevice(device)->name : "?",
			JoyAxisToken(axis), capture.invert ? " (inverted)" : "",
			delta, final_value);
		Sleep(800);		// let the control come back to rest
	}

	//
	// The pod's stick-head buttons, at their RIO addresses.
	//
	struct ButtonStep
	{
		const char	*prompt;
		int			address;
		int			allow_skip;
	};
	static const ButtonStep buttonSteps[] =
	{
		{ "MAIN: press the TRIGGER",                        0x40, 0 },
		{ "MIDDLE: press your second fire button",          0x46, 1 },
		{ "UPPER: press your third fire button",            0x47, 1 },
		{ "PINKY: press your fourth fire button",           0x45, 1 },
		{ "REVERSE: press the button for reverse thrust",   0x3F, 1 },
		{ "PANIC: press the button for the panic stop",     0x3D, 1 }
	};

	for (int s = 0; s < (int)(sizeof(buttonSteps) / sizeof(buttonSteps[0])); ++s)
	{
		printf("%s ... %s\n", buttonSteps[s].prompt,
			buttonSteps[s].allow_skip ? "(SPACE to skip)" : "");
		int device, button;
		int got = WizardCaptureButton(buttonSteps[s].allow_skip,
			&device, &button);
		if (got < 0)
		{
			printf("\nAborted - nothing written.\n");
			printf("Press any key to continue into the game.\n");
			_getch();
			return 1;
		}
		if (got == 0)
		{
			printf("  skipped.\n\n");
			continue;
		}
		WizardCapture &capture = captures[capture_count++];
		capture.used = 1;
		capture.device = device;
		capture.axis = -1;
		capture.button = button;
		sprintf(capture.line, "joybutton %d button 0x%02X",
			button, buttonSteps[s].address);
		printf("  -> device %d button %d\n\n", device, button);
		Sleep(400);
	}

	if (capture_count == 0)
	{
		printf("Nothing captured - nothing written.\n");
		printf("Press any key to continue into the game.\n");
		_getch();
		return 1;
	}

	//
	// Group by device into joydev slots, in order of first use. The hat
	// on the first device gets the pod's look cluster automatically -
	// hats are standardized, so there is nothing to ask.
	//
	int slot_of_device[joyMaxDevices];
	int slot_count = 0;
	for (int d = 0; d < joyMaxDevices; ++d)
	{
		slot_of_device[d] = -1;
	}
	for (int i = 0; i < capture_count; ++i)
	{
		if (captures[i].used && slot_of_device[captures[i].device] < 0)
		{
			slot_of_device[captures[i].device] = slot_count++;
		}
	}

	//
	// Rewrite bindings.txt, preserving everything outside the markers.
	//
	static const char *beginMarker =
		"# >>> RP412JOYCONFIG generated - do not edit between the markers";
	static const char *endMarker = "# <<< RP412JOYCONFIG end";

	static char kept[65536];
	kept[0] = '\0';
	{
		FILE *in = fopen("bindings.txt", "rt");
		if (in != NULL)
		{
			char line[512];
			int inside = 0;
			size_t used = 0;
			while (fgets(line, sizeof(line), in) != NULL)
			{
				if (strstr(line, "RP412JOYCONFIG generated") != NULL)
				{
					inside = 1;
					continue;
				}
				if (strstr(line, "RP412JOYCONFIG end") != NULL)
				{
					inside = 0;
					continue;
				}
				if (!inside && used + strlen(line) < sizeof(kept) - 1)
				{
					strcpy(kept + used, line);
					used += strlen(line);
				}
			}
			fclose(in);
		}
	}

	FILE *out = fopen("bindings.txt", "wt");
	if (out == NULL)
	{
		printf("ERROR: cannot write bindings.txt (wrong working directory?)\n");
		printf("Press any key to continue into the game.\n");
		_getch();
		return 1;
	}
	fputs(kept, out);
	if (kept[0] != '\0' && kept[strlen(kept) - 1] != '\n')
	{
		fputs("\n", out);
	}
	fprintf(out, "%s\n", beginMarker);
	int hat_done = 0;
	for (int d = 0; d < joyMaxDevices; ++d)
	{
		if (slot_of_device[d] < 0)
		{
			continue;
		}
		const RPJoyDeviceState *state = RPJoyDevice(d);
		fprintf(out, "joydev %d %s\n", slot_of_device[d],
			(state != NULL) ? state->name : "");
		for (int i = 0; i < capture_count; ++i)
		{
			if (captures[i].used && captures[i].device == d)
			{
				fprintf(out, "%s\n", captures[i].line);
			}
		}
		if (!hat_done)
		{
			hat_done = 1;
			fprintf(out, "joyhat 0 up    button 0x42\n");
			fprintf(out, "joyhat 0 down  button 0x41\n");
			fprintf(out, "joyhat 0 left  button 0x44\n");
			fprintf(out, "joyhat 0 right button 0x43\n");
		}
	}
	fprintf(out, "%s\n", endMarker);
	fclose(out);

	printf("bindings.txt written (%d controls + hat looks).\n", capture_count);
	printf("The game uses them from here on - have fun.\n\n");
	printf("Press any key to continue into the game.\n");
	_getch();
	return 0;
}
