お久しぶりのららいすです。
普段FF14をプレイする時は、左手デバイスに Razor Tartarus Proを、右手デバイスにG600を使用しています。

左手のデバイスとしては吉Pも愛用しているG13が有名ですが、
大きくて幅を取るため、ハードウェアとしては個人的にRazorの左手デバイスの方が使い勝手が良いのです。

ですが、Razorのデバイスを制御している Razor Synapseに、致命的な問題があります。
それは、時折バグで1度押して離したキーが押されたままの状態になるということです。
これはAmazon等のレビュー等でも問題視されていましたが、この不具合は昔からずっと放置されており、恐らく今後も直ることは無いんだろうと思います。

この不具合がFF14をプレイする際には致命的で、
WASDの移動でキーを離したあと、この不具合が発生すると永遠と上下左右いずれかの方法に歩き続けてしまうのです。
この不具合は押されている状態になっているキーを再度押下することで解消されるため、カジュアルなコンテンツでは問題ないのですが、絶などのシビアな移動が要求されるコンテンツでは外周ギリギリに立つつもりが不具合で外周にそのまま突っ込んでしまうなどの事故が発生してしまいます。
詠唱ジョブに至っては不具合を解消できるまで詠唱ができません。

また、この不具合が起こる確率はそこそこ高く、大体90分のプレイで1~3度ほど症状が発生します。
ハード自体は手に馴染んでいてとても使いやすいのですが、ソフトが大問題すぎる…。

この問題をどうにかすべく見つけたのが、こちらの記事です。
Razer Tartarus V2をRazer synapse3無しで動かす

これは、Tartarus Proの先代であるTartarus V2で、Cライブラリと自作PGを使用して、
Razor Synapseを使うことなくキー制御をできるようにするといった記事でした。

これを参考にして、私も自作APを作ってみることにしました。
まず、先述の記事のソースを丸コピして動かしてみましたが、スティックとスティックの上にあるボタン以外反応してくれません。
デバイスマネージャーを駆使して調べてみましたが、どうやら01~20キーとスティックは別のキーボードとして認識しているようでした。

原因さえわかってしまえば、対応は簡単なのでPGを修正していきます。
(スティックは厳密にはアナログではないのですが、いい変数名が思いつかなかったのでanalogpadとしました。


#include <windows.h>
#include <iostream>
#include <wchar.h>
#include "interception.h"
using namespace std;

//環境に合わせて変更してください。
const wchar_t KEYBD_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_01&Col01";
const wchar_t KEYBD_PAD[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_00";
const wchar_t MOUSE_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_02";

int main() {
    // 高優先度化
    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

    //Interceptionのインスタンス生成
    auto context = interception_create_context();
    if (!context)return 1;
    interception_set_filter(context, interception_is_keyboard,
        INTERCEPTION_FILTER_KEY_DOWN |
        INTERCEPTION_FILTER_KEY_UP |
        INTERCEPTION_KEY_E0 |
        INTERCEPTION_KEY_E1);
    interception_set_filter(context, interception_is_mouse,
        INTERCEPTION_MOUSE_WHEEL |
        INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_DOWN |
        INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_UP
        );


    //  Tartarus V2を探す
    InterceptionDevice keyboard = INTERCEPTION_MAX_DEVICE, analogpad = INTERCEPTION_MAX_DEVICE, mouse = INTERCEPTION_MAX_DEVICE;
    wchar_t buf[500];
    for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
        InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(KEYBD_HID, buf) == 0) {
            keyboard = d;
            break;
        }
    }
    for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
        InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(KEYBD_PAD, buf) == 0) {
            analogpad = d;
            break;
        }
    }
    for (InterceptionDevice i = 0;i < INTERCEPTION_MAX_MOUSE;i++) {
        InterceptionDevice d = INTERCEPTION_MOUSE(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(MOUSE_HID, buf) == 0) {
            mouse = d;
            break;
        }
    }
    if (keyboard == INTERCEPTION_MAX_DEVICE || mouse == INTERCEPTION_MAX_DEVICE || analogpad == INTERCEPTION_MAX_DEVICE) {
        interception_destroy_context(context);
        return 1;
    }

    // 入力を処理する
    InterceptionDevice device;
    InterceptionStroke stroke;
    while (interception_receive(context, device = interception_wait(context), &stroke, 1) > 0) {
        if (device == keyboard) {
            InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
            cout << "Keyboard Input "
                << "ScanCode=" << s.code
                << " State=" << s.state << endl;
        }
        else if (device == analogpad) {
            InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
            cout << "Keyboard Input "
                << "ScanCode=" << s.code
                << " State=" << s.state << endl;
        }
        else if (device == mouse) {
            InterceptionMouseStroke& s = *(InterceptionMouseStroke*)&stroke;
            cout << "Mouse Input"
                << " State=" << s.state
                << " Rolling=" << s.rolling
                << " Flags=" << s.flags
                << " (x,y)=(" << s.x << "," << s.y << ")"
                << endl;
        }
        else {
            //他のデバイスの入力は通過させる
            interception_send(context, device, &stroke, 1);
            if (interception_is_keyboard(device)) {//Escapeで終了
                InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
                if (s.code == 1) break;
            }
        }
    }

    interception_destroy_context(context);
    return 0;
}

これで全部のキーが認識されるようになったので、
続いてScanCodeの洗出しを行います。
ScanCodeはデバイスからPCに送られる、このキー押したよ!みたいな識別子みたいなものです。
ちなみにScanCodeにも種類があって、今回はXTっぽい。

下の画像は自分が分かる用に作ったもの。
コードの一覧はこちらのサイトを参考にしました。

キー押下時のScanCodeが分かったら、あとは個人用の設定をソースに組み込むだけですね!
最終的にはこんな感じになりました。


#include <windows.h>
#include <iostream>
#include <wchar.h>
#include "interception.h"
using namespace std;

//環境に合わせて変更してください。
const wchar_t KEYBD_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_01&Col01";
const wchar_t KEYBD_PAD[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_00";
const wchar_t MOUSE_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_02";

int main() {
    // 高優先度化
    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

    //Interceptionのインスタンス生成
    auto context = interception_create_context();
    if (!context)return 1;
    interception_set_filter(context, interception_is_keyboard,
        INTERCEPTION_FILTER_KEY_DOWN |
        INTERCEPTION_FILTER_KEY_UP |
        INTERCEPTION_KEY_E0 |
        INTERCEPTION_KEY_E1);
    interception_set_filter(context, interception_is_mouse,
        INTERCEPTION_MOUSE_WHEEL |
        INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_DOWN |
        INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_UP
        );


    //  Tartarus V2を探す
    InterceptionDevice keyboard = INTERCEPTION_MAX_DEVICE, analogpad = INTERCEPTION_MAX_DEVICE, mouse = INTERCEPTION_MAX_DEVICE;
    wchar_t buf[500];
    for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
        InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(KEYBD_HID, buf) == 0) {
            keyboard = d;
            break;
        }
    }
    for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
        InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(KEYBD_PAD, buf) == 0) {
            analogpad = d;
            break;
        }
    }
    for (InterceptionDevice i = 0;i < INTERCEPTION_MAX_MOUSE;i++) {
        InterceptionDevice d = INTERCEPTION_MOUSE(i);
        if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
            wcscmp(MOUSE_HID, buf) == 0) {
            mouse = d;
            break;
        }
    }
    if (keyboard == INTERCEPTION_MAX_DEVICE || mouse == INTERCEPTION_MAX_DEVICE || analogpad == INTERCEPTION_MAX_DEVICE) {
        interception_destroy_context(context);
        return 1;
    }

    // 入力を処理する
    InterceptionDevice device;
    InterceptionStroke stroke;
    while (interception_receive(context, device = interception_wait(context), &stroke, 1) > 0) {
        if (device == keyboard || device == analogpad) {
            InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
            bool isKeySend = true;
            switch (s.code)
            {
            case 2:
                s.code = 0x01;       //Esc
                break;

            case 3:
                s.code = 0x08;      //7
                break;

            case 4:
                s.code = 0x09;      //8
                break;

            case 5:
                s.code = 0x02;      //1
                break;

            case 6:
                s.code = 0x03;      //2

            case 15:
                break;

            case 16:
                break;

            case 17:
                break;

            case 18:
                s.code = 0x04;      //3
                break;

            case 19:
                s.code = 0x05;      //4
                break;

            case 58:
                isKeySend = false;
                s.code = 0x2A; interception_send(context, device, &stroke, 1);      //Shift
                s.code = 0x21; interception_send(context, device, &stroke, 1);      //F
                break;

            case 30:
                break;

            case 31:
                break;

            case 32:
                break;

            case 33:
                s.code = 0x06;      //5
                break;

            case 42:
                s.code = 0x2C;      //Z
                break;

            case 44:
                isKeySend = false; //無効化
                break;

            case 45:
                isKeySend = false; //無効化
                break;

            case 46:
                s.code = 0x2D;
                break;

            case 57:
                break;

            case 56:
                s.code = 0x07;      //6
                break;

            case 75:
                break;

            case 77:
                break;

            case 72:
                break;

            case 80:
                break;

            default:
                break;
            }

            /* isKeySend が false にされていない場合send1回 */
            if(isKeySend)
                interception_send(context, device, &stroke, 1);
        }
        //ホイールは既定を使うのでコメントアウト
        //else if (device == mouse) {
        //    InterceptionMouseStroke& s = *(InterceptionMouseStroke*)&stroke;
        //    cout << "Mouse Input"
        //        << " State=" << s.state
        //        << " Rolling=" << s.rolling
        //        << " Flags=" << s.flags
        //        << " (x,y)=(" << s.x << "," << s.y << ")"
        //        << endl;
        //}
        else {
            //他のデバイスの入力は通過させる
            interception_send(context, device, &stroke, 1);
            //if (interception_is_keyboard(device)) {//Escapeで終了
            //    InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
            //    if (s.code == 1) break;
            //    else
            //        cout << "Keyboard Input "
            //        << "ScanCode=" << s.code
            //        << " State=" << s.state << endl;
            //}
        }
    }

    interception_destroy_context(context);
    return 0;
}

あとはこれをコンパイルしてEXE起動してテスト。

結論:うまくいった。
勝ったな。

このままだと、起動時に常にコンソールが表示されて、コンソールを落とすとプログラムも終了してしまうので、Windowsアプリとして起動して、タスクトレイに常駐するようにしていきます。

C++ CLR空のプロジェクト(.NET Framework)でソリューションを再作成

とりあえず動けばいいよねって感じでコーディングしていく

MyForm.cpp


#include "MyForm.h"
using namespace RazorMyDriver;

[STAThreadAttribute]
int main()
{
    // 高優先度化
    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

	MyForm^ form = gcnew MyForm();
	form->ShowDialog();
	return 0;
}

MyForm.h


#pragma once
#include <Windows.h>
#include <Shellapi.h>
#include <thread>
#include <cstdio>
#include <cstdint>
#include <iostream>
#include <wchar.h>
#include "interception.h"

//環境に合わせて変更してください。
const wchar_t KEYBD_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_01&Col01";
const wchar_t KEYBD_PAD[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_00";
const wchar_t MOUSE_HID[] = L"HID\\VID_1532&PID_0244&REV_0200&MI_02";

namespace RazorMyDriver {

	using namespace System;
	using namespace System::ComponentModel;
	using namespace System::Collections;
	using namespace System::Windows::Forms;
	using namespace System::Data;
	using namespace System::Drawing;
	using namespace std;

	/// 
	/// MyForm の概要
	/// 
	public ref class MyForm : public System::Windows::Forms::Form
	{
	private: static uint32_t end_flag;

	public:
		MyForm(void)
		{
			InitializeComponent();
			//
			//TODO: ここにコンストラクター コードを追加します
			//
		}

	protected:
		/// 
		/// 使用中のリソースをすべてクリーンアップします。
		/// 
		~MyForm()
		{
			if (components)
			{
				delete components;
			}
		}
	private: System::Windows::Forms::NotifyIcon^ notifyIcon1;
	private: System::Windows::Forms::ContextMenuStrip^ contextMenuStrip1;
	private: System::Windows::Forms::ToolStripMenuItem^ endToolStripMenuItem;
	protected:
	private: System::ComponentModel::IContainer^ components;

	private:
		/// 
		/// 必要なデザイナー変数です。
		/// 


#pragma region Windows Form Designer generated code
		/// 
		/// デザイナー サポートに必要なメソッドです。このメソッドの内容を
		/// コード エディターで変更しないでください。
		/// 
		void InitializeComponent(void)
		{
			this->components = (gcnew System::ComponentModel::Container());
			System::ComponentModel::ComponentResourceManager^ resources = (gcnew System::ComponentModel::ComponentResourceManager(MyForm::typeid));
			this->notifyIcon1 = (gcnew System::Windows::Forms::NotifyIcon(this->components));
			this->contextMenuStrip1 = (gcnew System::Windows::Forms::ContextMenuStrip(this->components));
			this->endToolStripMenuItem = (gcnew System::Windows::Forms::ToolStripMenuItem());
			this->contextMenuStrip1->SuspendLayout();
			this->SuspendLayout();
			// 
			// notifyIcon1
			// 
			this->notifyIcon1->ContextMenuStrip = this->contextMenuStrip1;
			this->notifyIcon1->Icon = (cli::safe_cast(resources->GetObject(L"notifyIcon1.Icon")));
			this->notifyIcon1->Text = L"notifyIcon1";
			this->notifyIcon1->Visible = true;
			// 
			// contextMenuStrip1
			// 
			this->contextMenuStrip1->Items->AddRange(gcnew cli::array< System::Windows::Forms::ToolStripItem^  >(1) { this->endToolStripMenuItem });
			this->contextMenuStrip1->Name = L"contextMenuStrip1";
			this->contextMenuStrip1->Size = System::Drawing::Size(95, 26);
			// 
			// endToolStripMenuItem
			// 
			this->endToolStripMenuItem->Name = L"endToolStripMenuItem";
			this->endToolStripMenuItem->Size = System::Drawing::Size(94, 22);
			this->endToolStripMenuItem->Text = L"End";
			this->endToolStripMenuItem->Click += gcnew System::EventHandler(this, &MyForm::endToolStripMenuItem_Click);
			// 
			// MyForm
			// 
			this->AutoScaleDimensions = System::Drawing::SizeF(6, 12);
			this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
			this->ClientSize = System::Drawing::Size(284, 261);
			this->Name = L"MyForm";
			this->Text = L"MyForm";
			this->Load += gcnew System::EventHandler(this, &MyForm::MyForm_Load);
			this->contextMenuStrip1->ResumeLayout(false);
			this->ResumeLayout(false);

		}
#pragma endregion
	private: 
		System::Void endToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e) {
			end_flag = 0;       //終了を通知
            this->Close();      //自分はクローズ
		}
	private: 
		System::Void MyForm_Load(System::Object^ sender, System::EventArgs^ e) {
			// フォームを画面外に配置
			this->StartPosition = FormStartPosition::Manual;
			int screenWidth = GetSystemMetrics(SM_CXSCREEN);
			int screenHeight = GetSystemMetrics(SM_CYSCREEN);
			this->Location = Point(screenWidth + 1, screenHeight + 1);

			// フォームを非表示
			this->Hide();

			// タスクバーに表示しない
			HideTaskBar(true);

			//スレッドの作成
			end_flag = 1;
			std::thread th( MyForm::MonitorInput );

            th.detach();
		}

        public : static int MonitorInput()
        {
            //Interceptionのインスタンス生成
            auto context = interception_create_context();
            if (!context)return 1;
            interception_set_filter(context, interception_is_keyboard,
                INTERCEPTION_FILTER_KEY_DOWN |
                INTERCEPTION_FILTER_KEY_UP |
                INTERCEPTION_KEY_E0 |
                INTERCEPTION_KEY_E1);
            interception_set_filter(context, interception_is_mouse,
                INTERCEPTION_MOUSE_WHEEL |
                INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_DOWN |
                INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_UP
            );


            //  Tartarus V2を探す
            InterceptionDevice keyboard = INTERCEPTION_MAX_DEVICE, analogpad = INTERCEPTION_MAX_DEVICE, mouse = INTERCEPTION_MAX_DEVICE;
            wchar_t buf[500];
            for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
                InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
                if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
                    wcscmp(KEYBD_HID, buf) == 0) {
                    keyboard = d;
                    break;
                }
            }
            for (size_t i = 0;i < INTERCEPTION_MAX_KEYBOARD;i++) {
                InterceptionDevice d = INTERCEPTION_KEYBOARD(i);
                if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
                    wcscmp(KEYBD_PAD, buf) == 0) {
                    analogpad = d;
                    break;
                }
            }
            for (InterceptionDevice i = 0;i < INTERCEPTION_MAX_MOUSE;i++) {
                InterceptionDevice d = INTERCEPTION_MOUSE(i);
                if (interception_get_hardware_id(context, d, buf, sizeof(buf)) &&
                    wcscmp(MOUSE_HID, buf) == 0) {
                    mouse = d;
                    break;
                }
            }
            if (keyboard == INTERCEPTION_MAX_DEVICE || mouse == INTERCEPTION_MAX_DEVICE || analogpad == INTERCEPTION_MAX_DEVICE) {
                interception_destroy_context(context);
                return 1;
            }

            // 入力を処理する
            InterceptionDevice device;
            InterceptionStroke stroke;
            while (end_flag && interception_receive(context, device = interception_wait(context), &stroke, 1) > 0) {
                if (device == keyboard || device == analogpad) {
                    InterceptionKeyStroke& s = *(InterceptionKeyStroke*)&stroke;
                    bool isKeySend = true;
                    switch (s.code)
                    {
                    case 2:
                        s.code = 0x01;       //Esc
                        break;

                    case 3:
                        s.code = 0x08;      //7
                        break;

                    case 4:
                        s.code = 0x09;      //8
                        break;

                    case 5:
                        s.code = 0x02;      //1
                        break;

                    case 6:
                        s.code = 0x03;      //2

                    case 15:
                        break;

                    case 16:
                        break;

                    case 17:
                        break;

                    case 18:
                        s.code = 0x04;      //3
                        break;

                    case 19:
                        s.code = 0x05;      //4
                        break;

                    case 58:
                        isKeySend = false;
                        s.code = 0x2A; interception_send(context, device, &stroke, 1);      //Shift
                        s.code = 0x21; interception_send(context, device, &stroke, 1);      //F
                        break;

                    case 30:
                        break;

                    case 31:
                        break;

                    case 32:
                        break;

                    case 33:
                        s.code = 0x06;      //5
                        break;

                    case 42:
                        s.code = 0x2C;      //Z
                        break;

                    case 44:
                        isKeySend = false; //無効化
                        break;

                    case 45:
                        isKeySend = false; //無効化
                        break;

                    case 46:
                        s.code = 0x2D;
                        break;

                    case 57:
                        break;

                    case 56:
                        s.code = 0x07;      //6
                        break;

                    case 75:
                        break;

                    case 77:
                        break;

                    case 72:
                        break;

                    case 80:
                        break;

                    default:
                        break;
                    }

                    /* isKeySend が false にされていない場合send1回 */
                    if (isKeySend)
                        interception_send(context, device, &stroke, 1);
                }
                else {
                    //他のデバイスの入力は通過させる
                    interception_send(context, device, &stroke, 1);
                }
            }

            interception_destroy_context(context);
            return 0;
        }
		
	private:
		void HideTaskBar(bool hide)
		{
			HANDLE hWnd = FindWindow(L"Shell_TrayWnd", NULL);
			APPBARDATA ABData;
			ABData.cbSize = sizeof(ABData);
			ABData.hWnd = (HWND)hWnd;
			ABData.lParam = (hide ? ABS_AUTOHIDE : ABS_ALWAYSONTOP);
			SHAppBarMessage(ABM_SETSTATE, &ABData);
		}
	};
}

完成です!!お疲れさまでした!

いやぁ~これで長年の悩みから開放されます!!
アクチュエーションポイントの設定とか、その他の設定はできないけど、
そもそも既定の設定から変えていないので問題ナシ!

ちなみに、Razor Synapse3がスタートアップに入っていると、うまく動かなくなってしまうので、
実際に使用する場合、Razor Synapse3はアンインストールするか、スタートアップを無効にしておきましょう!

Razor Tartarus Proは、自分でデバイスを制御できるのであればオススメの一品です。
特に、メカニカルではなくメカメンブレンという独特なキーを採用しており、押し心地はメンブレンに近いので、メカニカルが苦手な方にオススメ!