diff --git a/README.md b/README.md index 4084946..5610efb 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,29 @@ chuniio driver for tasoller custom 2.0 firmware -Uses WinUSB driver instead of libusb - -Written in Zig (Sorry, I can't stand Windows-flavoured C++) +Supported titles: +- Chunithm +- Chunithm NEW Downloads avaliable in [releases](https://dev.s-ul.net/akiroz/chuniio-tasoller/-/releases) +## Configuration + +segatools.ini +```ini +[chuniio] +path=chuniio_tasoller.dll +; path=chuniio_tasoller_x64.dll + +;; For Chunithm NEW, set to 1 +chusan=0 + +[io3] +test=0x31 +service=0x32 +coin=0x33 +``` + ## USB Protocol Custom firmware USB device: 1CCF:2333 diff --git a/build.zig b/build.zig index 4e1380b..c704f15 100644 --- a/build.zig +++ b/build.zig @@ -2,15 +2,19 @@ const std = @import("std"); const CrossTarget = std.zig.CrossTarget; pub fn build(b: *std.build.Builder) void { - const target = CrossTarget{ .os_tag = .windows, .cpu_arch = .i386, .abi = .msvc }; - const lib = b.addSharedLibrary("chuniio_tasoller", "src/main.zig", .unversioned); - lib.setBuildMode(b.standardReleaseOptions()); - lib.setTarget(target); - lib.install(); + const lib86 = b.addSharedLibrary("chuniio_tasoller", "src/main.zig", .unversioned); + lib86.setBuildMode(b.standardReleaseOptions()); + lib86.setTarget(CrossTarget{ .os_tag = .windows, .cpu_arch = .i386, .abi = .msvc }); + lib86.install(); + + const lib64 = b.addSharedLibrary("chuniio_tasoller_x64", "src/main.zig", .unversioned); + lib64.setBuildMode(b.standardReleaseOptions()); + lib64.setTarget(CrossTarget{ .os_tag = .windows, .cpu_arch = .x86_64, .abi = .msvc }); + lib64.install(); - const exe = b.addExecutable("tasoller_test", "src/main.zig"); - lib.setBuildMode(b.standardReleaseOptions()); - exe.setTarget(target); - exe.install(); + // const exe = b.addExecutable("tasoller_test", "src/main.zig"); + // lib.setBuildMode(b.standardReleaseOptions()); + // exe.setTarget(target); + // exe.install(); } diff --git a/segatools.ini b/segatools.ini index ed89afe..adb0298 100644 --- a/segatools.ini +++ b/segatools.ini @@ -1,3 +1,6 @@ +[chuniio] +chusan=1 + [io3] test=0x31 service=0x32 diff --git a/src/main.zig b/src/main.zig index c08c9d7..3875404 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,12 @@ +const builtin = @import("builtin"); const std = @import("std"); const win32 = @import("zigwin32/win32.zig"); +const W = std.unicode.utf8ToUtf16LeStringLiteral; +const HANDLE = std.os.windows.HANDLE; +const LPVOID = std.os.windows.LPVOID; const DWORD = std.os.windows.DWORD; +const BOOL = win32.foundation.BOOL; const HRESULT = std.os.windows.HRESULT; const S_OK = std.os.windows.S_OK; const E_FAIL = std.os.windows.E_FAIL; @@ -10,6 +15,12 @@ const GetLastError = win32.foundation.GetLastError; const GetAsyncKeyState = win32.ui.input.keyboard_and_mouse.GetAsyncKeyState; const GetPrivateProfileIntA = win32.system.windows_programming.GetPrivateProfileIntA; +const PAGE_READWRITE = win32.system.memory.PAGE_READWRITE; +const FILE_MAP_ALL_ACCESS = win32.system.memory.FILE_MAP_ALL_ACCESS; +const CreateFileMapping = win32.system.memory.CreateFileMapping; +const MapViewOfFile = win32.system.memory.MapViewOfFile; + +const DLL_PROCESS_ATTACH = win32.system.system_services.DLL_PROCESS_ATTACH; const GENERIC_READ = win32.system.system_services.GENERIC_READ; const GENERIC_WRITE = win32.system.system_services.GENERIC_WRITE; const OPEN_EXISTING = win32.storage.file_system.OPEN_EXISTING; @@ -32,60 +43,13 @@ const WinUsb_Initialize = win32.devices.usb.WinUsb_Initialize; const WinUsb_WritePipe = win32.devices.usb.WinUsb_WritePipe; const WinUsb_ReadPipe = win32.devices.usb.WinUsb_ReadPipe; -const chuni_io_slider_callback_t = ?fn ([*c]const u8) callconv(.C) void; - const Config = struct { test_key: i32, serv_key: i32, coin_key: i32, + chusan: i32, }; -pub fn main() !void { - var ground = usb_out[4*3 .. 29*3]; - var left = usb_out[32*3 .. 56*3]; - var right = usb_out[56*3 .. 80*3]; - var i: u32 = 0; - while(i < 32) : (i += 1) { - if(i < ground.len/3) { - ground[i*3 + 1] = 0x13; - ground[i*3 + 0] = 0x7a; - ground[i*3 + 2] = 0x5f; - if(i % 2 == 0) { - ground[i*3 + 1] = 0x66; - ground[i*3 + 0] = 0xee; - ground[i*3 + 2] = 0xab; - } - if(i % 4 == 0) { - ground[i*3 + 1] = 0xe1; - ground[i*3 + 0] = 0x28; - ground[i*3 + 2] = 0x85; - } - } - if(i < left.len/3) { - left[i*3 + 1] = 0x13; - left[i*3 + 0] = 0x7a; - left[i*3 + 2] = 0x5f; - } - if(i < right.len/3) { - right[i*3 + 1] = 0x13; - right[i*3 + 0] = 0x7a; - right[i*3 + 2] = 0x5f; - } - } - try tasoller_init(); - while(true) { - if(WinUsb_WritePipe(tasoller, 0x03, @ptrCast(*u8, &usb_out), usb_out.len, &i, null) == 0) { - std.log.warn("[chuniio] WinUsb_WritePipe: {any}", .{GetLastError()}); - } - if(WinUsb_ReadPipe(tasoller, 0x84, @ptrCast(*u8, &usb_in), usb_in.len, &i, null) == 0) { - std.log.warn("[chuniio] WinUsb_ReadPipe: {any}", .{GetLastError()}); - } - std.time.sleep(100_000_000); - } -} - -// MAIN DRIVER ====================================================================================================== - var cfg: ?Config = null; var thread_op = std.Thread.Mutex{}; var slider_active = false; @@ -94,9 +58,28 @@ var input_thread: ?std.Thread = null; var usb_out_op = std.Thread.Mutex{}; var usb_out = std.mem.zeroes([80*3]u8); -var usb_in = std.mem.zeroes([0x24]u8); +var usb_in: ?[]u8 = null; var tasoller: ?*anyopaque = null; +// Test program +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + usb_in = try gpa.allocator().alloc(u8, 0x24); + var i: u32 = 0; + try tasoller_init(); + while(true) { + if(WinUsb_WritePipe(tasoller, 0x03, @ptrCast(*u8, &usb_out), usb_out.len, &i, null) == 0) { + std.log.warn("[chuniio] WinUsb_WritePipe: {any}", .{GetLastError()}); + } + if(WinUsb_ReadPipe(tasoller, 0x84, usb_in.ptr, usb_in.len, &i, null) == 0) { + std.log.warn("[chuniio] WinUsb_ReadPipe: {any}", .{GetLastError()}); + } + std.time.sleep(100_000_000); + } +} + +// MAIN DRIVER ====================================================================================================== + fn tasoller_init() !void { const hDevInfo = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_USB_DEVICE, null, null, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); @@ -112,7 +95,7 @@ fn tasoller_init() !void { var devicePath: ?[*:0]const u8 = null; while(SetupDiEnumDeviceInterfaces(hDevInfo, null, &GUID_DEVINTERFACE_USB_DEVICE, ifIdx, &devIf) != 0) : (ifIdx += 1) { var requiredSize: u32 = 0; - var detailBuf = std.mem.zeroes([263]u8); + var detailBuf align(4) = std.mem.zeroes([512]u8); var devIfDetail = @ptrCast(*SP_DEVICE_INTERFACE_DETAIL_DATA_A, &detailBuf); devIfDetail.cbSize = @sizeOf(SP_DEVICE_INTERFACE_DETAIL_DATA_A); if(SetupDiGetDeviceInterfaceDetailA(hDevInfo, &devIf, devIfDetail, 263, &requiredSize, null) == 0) { @@ -163,18 +146,21 @@ fn tasoller_init() !void { // Poll input regardless of slider start/stop fn input_thread_proc() void { + std.log.info("[chuniio] Input thread started", .{}); while(true) { var len: u32 = 0; - if(WinUsb_ReadPipe(tasoller, 0x84, @ptrCast(*u8, &usb_in), usb_in.len, &len, null) == 0) { + if(WinUsb_ReadPipe(tasoller, 0x84, @ptrCast(*u8, usb_in.?.ptr), usb_in.?.len, &len, null) == 0) { std.log.warn("[chuniio] WinUsb_ReadPipe: {any}", .{GetLastError()}); } } } +const chuni_io_slider_callback_t = ?fn ([*c]const u8) callconv(.C) void; + fn slider_thread_proc(callback: chuni_io_slider_callback_t) void { var pressure = std.mem.zeroes([32]u8); while(slider_active) { - for(usb_in[4..]) |byte, i| { + for(usb_in.?[4..]) |byte, i| { // Tasoller order: top->bottom, left->right // Chunithm order: top->bottom, right->left pressure[if(i%2 == 0) 30-i else 32-i] = byte; @@ -184,37 +170,77 @@ fn slider_thread_proc(callback: chuni_io_slider_callback_t) void { } } +// DLL EXPORTS ====================================================================================================== + export fn chuni_io_get_api_version() c_ushort { return 0x0101; } -export fn chuni_io_jvs_init() HRESULT { +pub export fn DllMain(hDllHandle: HANDLE, dwReason: DWORD, lpreserved: LPVOID) BOOL { + _ = hDllHandle; + _ = lpreserved; + + if(dwReason != DLL_PROCESS_ATTACH) return win32.zig.TRUE; + std.log.info("[chuniio] Initializing", .{}); + const cfg_file = ".\\segatools.ini"; cfg = .{ .test_key = @intCast(i32, GetPrivateProfileIntA("io3", "test", 0x31, cfg_file)), .serv_key = @intCast(i32, GetPrivateProfileIntA("io3", "service", 0x32, cfg_file)), .coin_key = @intCast(i32, GetPrivateProfileIntA("io3", "coin", 0x33, cfg_file)), + .chusan = @intCast(i32, GetPrivateProfileIntA("chuniio", "chusan", 0, cfg_file)), }; - tasoller_init() catch { - return E_FAIL; - }; - input_thread = std.Thread.spawn(.{}, input_thread_proc, .{}) catch |err| { - std.log.err("[chuniio] Spawn input thread: {any}", .{err}); - return E_FAIL; - }; + + if(cfg.?.chusan == 0) { + std.log.info("[chuniio] Mode: chuni", .{}); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + usb_in = gpa.allocator().alloc(u8, 0x24) catch |err| { + std.log.err("[chuniio] alloc: {any}", .{err}); + return win32.zig.FALSE; + }; + } else { + std.log.info("[chuniio] Mode: chusan", .{}); + const szName = W("Global\\ChuniioTasoller"); + const hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, null, PAGE_READWRITE, 0, 0x24, szName) orelse { + std.log.err("[chuniio] CreateFileMapping: {any}", .{GetLastError()}); + return win32.zig.FALSE; + }; + const pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0x24) orelse { + std.log.err("[chuniio] MapViewOfFile: {any}", .{GetLastError()}); + return win32.zig.FALSE; + }; + usb_in = @ptrCast([*]u8, pBuf)[0..0x24]; + } + + if(builtin.cpu.arch == .i386) { + tasoller_init() catch { + return win32.zig.FALSE; + }; + input_thread = std.Thread.spawn(.{}, input_thread_proc, .{}) catch |err| { + std.log.err("[chuniio] Spawn input thread: {any}", .{err}); + return win32.zig.FALSE; + }; + } + + return win32.zig.TRUE; +} + +export fn chuni_io_jvs_init() HRESULT { return S_OK; } export fn chuni_io_jvs_poll(opbtn: ?[*]u8, beams: ?[*]u8) void { + if(cfg.?.chusan != 0 and builtin.cpu.arch != .x86_64) return; if(opbtn == null or beams == null) return; - if(GetAsyncKeyState(cfg.?.test_key) != 0 or (usb_in[3] & (1 << 7)) != 0) opbtn.?.* |= (1 << 0); - if(GetAsyncKeyState(cfg.?.test_key) != 0 or (usb_in[3] & (1 << 6)) != 0) opbtn.?.* |= (1 << 1); - beams.?.* |= usb_in[3] & 0b111111; + if(GetAsyncKeyState(cfg.?.test_key) != 0 or (usb_in.?[3] & (1 << 7)) != 0) opbtn.?.* |= (1 << 0); + if(GetAsyncKeyState(cfg.?.test_key) != 0 or (usb_in.?[3] & (1 << 6)) != 0) opbtn.?.* |= (1 << 1); + beams.?.* |= usb_in.?[3] & 0b111111; } var coin_conter: c_ushort = 0; var coin_prev_depressed = false; export fn chuni_io_jvs_read_coin_counter(total: ?*c_ushort) void { + if(cfg.?.chusan != 0 and builtin.cpu.arch != .x86_64) return; if(total == null) return; const coin_depressed = GetAsyncKeyState(cfg.?.coin_key) != 0; if(coin_depressed and !coin_prev_depressed) coin_conter += 1; @@ -227,7 +253,9 @@ export fn chuni_io_slider_init() HRESULT { } export fn chuni_io_slider_start(callback: chuni_io_slider_callback_t) void { + if(cfg.?.chusan != 0 and builtin.cpu.arch != .i386) return; if(callback == null) return; + thread_op.lock(); defer thread_op.unlock(); if(slider_thread == null) { @@ -240,6 +268,8 @@ export fn chuni_io_slider_start(callback: chuni_io_slider_callback_t) void { } export fn chuni_io_slider_stop() void { + if(cfg.?.chusan != 0 and builtin.cpu.arch != .i386) return; + thread_op.lock(); defer thread_op.unlock(); if(slider_thread != null) { @@ -250,6 +280,7 @@ export fn chuni_io_slider_stop() void { } export fn chuni_io_slider_set_leds(rgb: ?[*]u8) void { + if(cfg.?.chusan != 0 and builtin.cpu.arch != .i386) return; if(rgb == null) return; var n: u32 = 0;