#include "printer.h" #include #include #include "avs/game.h" #include "hooks/sleephook.h" #include "hooks/libraryhook.h" #include "launcher/launcher.h" #include "util/detour.h" #include "util/fileutils.h" #include "util/libutils.h" #include "util/logging.h" #include "util/utils.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "external/stb_image_write.h" namespace games::shared { // settings std::vector PRINTER_PATH; std::vector PRINTER_FORMAT; int PRINTER_JPG_QUALITY = 85; bool PRINTER_CLEAR = false; bool PRINTER_OVERWRITE_FILE = false; static DWORD PRINTER_TOTAL_COUNT = 1024; static short PRINTER_PAPER_REMAIN = 4096; typedef struct tagCPDIDinfo { short usbNo; short printerID; CHAR serialNo[6]; WORD mediaType; } CPDIDinfo, *PCPDIDinfo; typedef struct tagCPDPaperRemainParams { short restNum; BOOL isWarning; } CPDPaperRemainParams, *PCPDPaperRemainParams; typedef struct tagCP9StatusInfo { DWORD printerStatus; DWORD detail; DWORD reserved2; DWORD reserved3; } CP9StatusInfo, *PCP9StatusInfo; typedef struct tagCP9FWInfo { char version_info1[6]; char version_info2[6]; char version_info3[6]; } CP9FWInfo, *PCP9FWInfo; typedef struct tagCP9PrinterParams2 { DWORD flags1; DWORD reserved1; short mechaPreFeed; short jobResume; short TransferOnPrinting; } CP9PrinterParams2, *PCP9PrinterParams2; typedef struct tagCPDMediaType { WORD mediaType; WORD mediaDetail; WORD reserved; } CPDMediaType, *PCPDMediaType; typedef struct tagCPAPrinterParams { DWORD ver; DWORD flags1; DWORD reserved1; POINTS printPixel; short sidePrint; WORD printCount; short overCoat; short mirror; short marginCut; short multiCut; short multipanel; short printOut; short reserved4; short inkSkip; } CPAPrinterParams, *PCPAPrinterParams; typedef struct tagCPDBandImageParams { PVOID baseAddr; long rowBytes; RECT bounds; } CPDBandImageParams, *PCPDBandImageParams; typedef struct tagCPDContrastTable { BYTE r[256]; BYTE g[256]; BYTE b[256]; } CPDContrastTable, *PCPDContrastTable; typedef struct tagCPDGammaTable { WORD r[256]; WORD g[256]; WORD b[256]; } CPDGammaTable, *PCPDGammaTable; typedef struct tagCPAImageEffectParams { DWORD ver; short ColorTabel; short DLLColorTabel; short ContrastTabel; const CPDContrastTable *pContTbl; short sharpness0; const BYTE *pSharpnessTbl; short sharpness1; short gamma; short printMode; short linesharpness; const CPDGammaTable *pGammaTbl; short overcoatMode; } CPAImageEffectParams, *PCPAImageEffectParams; enum { PStatus_Ready = 0, PStatus_Printing, PStatus_MechaInit, PStatus_FeedandCut }; enum { Error_NoError = 0, Error_Something, Error_DeviceNotFound = 100, Error_Busy, Error_Printing_Busy, Error_Printing_Ready, Error_PortBusy, Error_GetCPDIDinfo, Error_PrinterBusy, Error_FuncParamError = 201, Error_MemAllocError, Error_Timeout, Error_MattePrinter = 301, Error_MatteRibbon, Error_InvalidParam = 1000, Error_SheetEnd, Error_PaperEnd, Error_PaperJam, Error_SheetJam, Error_SheetCassetNotSet, Error_PaperSheetIllegal, Error_PaperCassetNotSet, Error_PaperSizeIllegal, Error_PaperTrayNotSet, Error_OHPReverse, Error_HeatError, Error_DewError, Error_DoorOpen, Error_UnusableSheet, Error_SheetCassetIllegal, Error_PaperRemain, Error_SheetRemain, Error_NotSupported, Error_SheetMarkError = 1101, Error_PaperJam_D, Error_MechaError, Error_MechaError_D, Error_MechaInitReq, Error_PrintingTurnOff, Error_ContrastDataError, Error_TableError, Error_PrinterError, Error_PickPosition, Error_NoScrapBox, Error_PrintingDoorOpen, Error_SheetError, Error_SheetCountError, Error38_HeadVoltage = 1200, Error38_HeadPosition, Error38_FunStopped, Error38_CutterError, Error38_PinchRollerPosition, Error38_HeadTemp, Error38_MediaTemp, Error38_PaperWindingMorterTemp, Error38_RibbonTension = 1210, Error38_RFID_Error = 1220, Error38_SystemError = 1230, Error_JInvalidParam = 1300, Error_JMemoryFull, Error_JPaperSizeIllegal = 1310, Error_JLastJobError, Error_JTimeout = 1320, Error_JJobCancel, Error_JUSBInterrupt, }; static DWORD __stdcall CPU9CheckPaperRemain(PCPDPaperRemainParams pPaperRemain, PCPDIDinfo pIDInfo) { pPaperRemain->restNum = PRINTER_PAPER_REMAIN; pPaperRemain->isWarning = FALSE; return Error_NoError; } static DWORD __stdcall CPU9CheckPrinter(PCP9StatusInfo pStInfo, PCPDIDinfo pIDInfo) { pStInfo->printerStatus = PStatus_Ready; pStInfo->detail = 0; return Error_NoError; } static DWORD __stdcall CPU9CheckPrintEnd(DWORD meminfo, PBOOL pbisEnd, PCPDIDinfo pIDInfo) { *pbisEnd = true; return Error_NoError; } static DWORD __stdcall CPU9GetFWInfo(PCP9FWInfo pFWInfo, PCPDIDinfo pIDInfo) { strcpy(pFWInfo->version_info1, std::string("1").c_str()); strcpy(pFWInfo->version_info2, std::string("0").c_str()); strcpy(pFWInfo->version_info3, std::string("1").c_str()); return Error_NoError; } static DWORD __stdcall CPU9GetMediaType(PCPDMediaType pMType, PCPDIDinfo pIDInfo) { if (avs::game::is_model("KLP")) { pMType->mediaType = 56; } else { pMType->mediaType = 2; } pMType->mediaDetail = 18432; return Error_NoError; } static DWORD __stdcall CPU9GetTempInfo(void *pTempInfo, PCPDIDinfo pIDInfo) { return Error_NoError; } static DWORD __stdcall CPU9GetTotalPrintCount(PDWORD pdwCount, PCPDIDinfo pIDInfo) { *pdwCount = PRINTER_TOTAL_COUNT; return Error_NoError; } static DWORD __stdcall CPU9PreHeat(PCPDIDinfo pIDInfo) { return Error_NoError; } static DWORD __stdcall CPU9PrintJobCancel(PCPDIDinfo pIDInfo) { return Error_NoError; } static DWORD __stdcall CPU9PrintOut(PDWORD pmeminfo, PCPDIDinfo pIDInfo) { // do logic if (PRINTER_PAPER_REMAIN > 0) { PRINTER_TOTAL_COUNT++; PRINTER_PAPER_REMAIN--; return Error_NoError; } else { return Error_PaperRemain; } } static DWORD __stdcall CPU9SetPrintParameter2(const CP9PrinterParams2 *setP, PCPDIDinfo pIDInfo) { return Error_NoError; } typedef void (CALLBACK *PFNACBfunc)(DWORD dwErr, short PrintFlag, short PrintUsbNo, long nJid, int CopyNumber); static inline std::string get_image_out_path(bool clear, std::string path, const std::string &format) { // check path if (path.empty()) { log_warning("printer", "Printer Emulation output directory can't be empty. Resetting to \".\""); path = "."; } // check trailing slash if (string_ends_with(path.c_str(), "\\")) path = std::string(path.c_str(), path.length() - 1); // find non-existing filename static std::string prefix = "printer_"; for (int n = 0; n < 4096; n++) { std::ostringstream filename_s; filename_s << path << "\\" << prefix << n << "." << format; std::string filename = filename_s.str(); if (clear && fileutils::file_exists(filename.c_str())) { log_info("printer", "deleting {}...", filename); DeleteFile(filename.c_str()); if (PRINTER_OVERWRITE_FILE) { return filename; } } else if (!fileutils::file_exists(filename.c_str())) return filename; } // error if (!clear) { log_fatal("sdvx", "could not find path"); } return "DUMMY"; } static inline bool process_image_print(const CPDBandImageParams *pBandImage) { // log log_info("printer", "processing incoming print job"); // get image bounds int image_width = pBandImage->bounds.right - pBandImage->bounds.left; int image_height = pBandImage->bounds.bottom - pBandImage->bounds.top; // check bounds if (image_width <= 0 || image_height <= 0) { log_warning("printer", "invalid image size: {}x{}", image_width, image_height); return false; } // check rowBytes if (pBandImage->rowBytes < 0 || pBandImage->rowBytes != image_width * 3) { log_warning("printer", "unsupported image data layout: {}", pBandImage->rowBytes); return false; } // make a copy of the image data auto image_data = new uint8_t[image_width * image_height * 3]; memcpy(image_data, pBandImage->baseAddr, (size_t) (image_width * image_height * 3)); // convert BGR to RGB log_info("printer", "converting BGR to RGB..."); for (int pixel = 0; pixel < image_width * image_height; pixel++) { int index = pixel * 3; uint8_t tmp = image_data[index]; image_data[index] = image_data[index + 2]; image_data[index + 2] = tmp; } // flip horizontally for (int x = 0; x < image_width / 2; x++) { for (int y = 0; y < image_height; y++) { int index1 = (y * image_width + x) * 3; int index2 = (y * image_width + image_width - x - 1) * 3; uint8_t r = image_data[index1 + 0]; uint8_t g = image_data[index1 + 1]; uint8_t b = image_data[index1 + 2]; image_data[index1 + 0] = image_data[index2 + 0]; image_data[index1 + 1] = image_data[index2 + 1]; image_data[index1 + 2] = image_data[index2 + 2]; image_data[index2 + 0] = r; image_data[index2 + 1] = g; image_data[index2 + 2] = b; } } // iterate folders log_info("printer", "writing files..."); for (const auto &path : PRINTER_PATH) { for (const auto &format : PRINTER_FORMAT) { // get image path std::string image_path = get_image_out_path(PRINTER_OVERWRITE_FILE, path, format); bool success = false; // call write function depending on format if (format == "png" && stbi_write_png( image_path.c_str(), image_width, image_height, 3, image_data, image_width * 3)) success = true; if (format == "bmp" && stbi_write_bmp( image_path.c_str(), image_width, image_height, 3, image_data)) success = true; if (format == "tga" && stbi_write_tga( image_path.c_str(), image_width, image_height, 3, image_data)) success = true; if (format == "jpg" && stbi_write_jpg( image_path.c_str(), image_width, image_height, 3, image_data, PRINTER_JPG_QUALITY)) success = true; // logging if (success) { log_info("printer", "printer emulation has written an image to {}", image_path); } else { log_warning("printer", "printer emulation failed to write image to {}", image_path); } } } // clean up delete[] image_data; return true; } static DWORD __stdcall CPUASendImage( const CPDBandImageParams *pBandImage, const CPAPrinterParams *setP, const CPAImageEffectParams *piep, PCPDIDinfo pIDInfo ) { // process image if (!process_image_print(pBandImage)) { return Error_InvalidParam; } return Error_NoError; } /* * This is the function which seems to get called from the game. * Default card layout parameters: * rowBytes = 3216 * bounds.left = 0 * bounds.top = 0 * bounds.right = 1072 * bounds.bottom = 712 */ static DWORD __stdcall CPUASendImagePrint(const CPAPrinterParams *setP, const CPDBandImageParams *pBandImage, const CPAImageEffectParams *piep, BOOL memClear, PFNACBfunc pfncb, long nJid, PCPDIDinfo pIDInfo) { /* * From documentation: rowBytes * Number of bits a line of image data. Usually, in case of a bitmap file of 24bit, it is * (bmInfoHeader.biWidth * 3 + 3) / 4 * 4 * It scans up from the lower left in case of a positive numeric (bitmap file standard). * In this case, the address at the lower left of image data is usually set in baseAddr. * It scans below from the upper left in the case of a negative numeric. In this case, * The address in the upper left of image data is usually set in baseAddr. */ // process image if (!process_image_print(pBandImage)) { return Error_InvalidParam; } // fire up printer thread // the game fires up a listener around 4 seconds after the call WORD printCount = setP->printCount; short usbNo = pIDInfo->usbNo; std::thread t([printCount, pfncb, usbNo, nJid]() { for (int print_no = 1; print_no <= printCount; print_no++) { // wait for game listener Sleep(4000); // do logic if (PRINTER_PAPER_REMAIN > 0) { PRINTER_TOTAL_COUNT++; PRINTER_PAPER_REMAIN--; pfncb(Error_NoError, 0, usbNo, nJid, print_no); } else { pfncb(Error_PaperRemain, 0, usbNo, nJid, print_no); } } }); // detach thread so it will keep running t.detach(); return Error_NoError; } /* * Probably irrelevant, doesn't seem to get called by the game. */ static DWORD __stdcall CPUASendImagePrint2( const CPAPrinterParams *setP, const CPDBandImageParams *pBandImage, const CPAImageEffectParams *piep, BOOL memClear, BOOL sendOnPrn, PFNACBfunc pfncb, long nJid, PCPDIDinfo pIDInfo ) { // forward to other function return CPUASendImagePrint(setP, pBandImage, piep, memClear, pfncb, nJid, pIDInfo); } static DWORD __stdcall CPUASetPrintParameter( const CPAPrinterParams *setP, CPAImageEffectParams *piep, BOOL memClear, PDWORD pmeminfo, PCPDIDinfo pIDInfo ) { return Error_NoError; } static DWORD __stdcall CPUXSearchPrinters( PCPDIDinfo pIDInfo, DWORD infoSize, LPDWORD pSizeNeeded, LPDWORD pInfoNum ) { // set information number // (LovePlus needs this to determine how many info structures to allocate) *pInfoNum = 1; // check info size if (infoSize != 12) { *pSizeNeeded = 12; return Error_GetCPDIDinfo; } // set printer information pIDInfo->usbNo = 1; pIDInfo->printerID = 1; memset(pIDInfo->serialNo, 'F', 5); memset(pIDInfo->serialNo + 5, 0, 1); pIDInfo->mediaType = 2; // LovePlus if (avs::game::is_model("KLP")) { // LovePlus uses a different media type pIDInfo->mediaType = 56; } // Otoca D'or if (avs::game::is_model("NCG")) { // requires a specific printer ID pIDInfo->printerID = 2160; } return Error_NoError; } static void __stdcall CPUXInit() { log_info("printer", "CPUXInit called"); } void printer_attach() { log_info("printer", "SpiceTools Printer"); // default parameters if (PRINTER_PATH.empty()) { PRINTER_PATH.emplace_back("."); } if (PRINTER_FORMAT.empty()) { PRINTER_FORMAT.emplace_back("png"); } // validate file formats for (const auto &format : PRINTER_FORMAT) { if (format != "png" && format != "bmp" && format != "tga" && format != "jpg") { log_fatal("printer", "unknown file format: {}", format); } } // validate JPEG quality if (PRINTER_JPG_QUALITY <= 0 || PRINTER_JPG_QUALITY > 100) { log_fatal("printer", "invalid JPEG quality setting (1-100): {}", PRINTER_JPG_QUALITY); } // clear if (PRINTER_CLEAR) { PRINTER_CLEAR = false; for (const auto &path : PRINTER_PATH) { for (const auto &format : PRINTER_FORMAT) { get_image_out_path(true, path, format); } } } // IAT hooks detour::iat_try("CPU9CheckPaperRemain", CPU9CheckPaperRemain); detour::iat_try("CPU9CheckPrinter", CPU9CheckPrinter); detour::iat_try("CPU9CheckPrintEnd", CPU9CheckPrintEnd); detour::iat_try("CPU9GetFWInfo", CPU9GetFWInfo); detour::iat_try("CPU9GetMediaType", CPU9GetMediaType); detour::iat_try("CPU9GetTempInfo", CPU9GetTempInfo); detour::iat_try("CPU9GetTotalPrintCount", CPU9GetTotalPrintCount); detour::iat_try("CPU9PreHeat", CPU9PreHeat); detour::iat_try("CPU9PrintJobCancel", CPU9PrintJobCancel); detour::iat_try("CPU9PrintOut", CPU9PrintOut); detour::iat_try("CPU9SetPrintParameter2", CPU9SetPrintParameter2); detour::iat_try("CPUASendImage", CPUASendImage); detour::iat_try("CPUASendImagePrint", CPUASendImagePrint); detour::iat_try("CPUASendImagePrint2", CPUASendImagePrint2); detour::iat_try("CPUASetPrintParameter", CPUASetPrintParameter); detour::iat_try("CPUXInit", CPUXInit); detour::iat_try("CPUXSearchPrinters", CPUXSearchPrinters); // library hook libraryhook_hook_library("CPUSBXPKM.DLL", GetModuleHandle(nullptr)); libraryhook_hook_proc("CPU9CheckPaperRemain", CPU9CheckPaperRemain); libraryhook_hook_proc("CPU9CheckPrinter", CPU9CheckPrinter); libraryhook_hook_proc("CPU9CheckPrintEnd", CPU9CheckPrintEnd); libraryhook_hook_proc("CPU9GetFWInfo", CPU9GetFWInfo); libraryhook_hook_proc("CPU9GetMediaType", CPU9GetMediaType); libraryhook_hook_proc("CPU9GetTempInfo", CPU9GetTempInfo); libraryhook_hook_proc("CPU9GetTotalPrintCount", CPU9GetTotalPrintCount); libraryhook_hook_proc("CPU9PreHeat", CPU9PreHeat); libraryhook_hook_proc("CPU9PrintJobCancel", CPU9PrintJobCancel); libraryhook_hook_proc("CPU9PrintOut", CPU9PrintOut); libraryhook_hook_proc("CPU9SetPrintParameter2", CPU9SetPrintParameter2); libraryhook_hook_proc("CPUASendImage", CPUASendImage); libraryhook_hook_proc("CPUASendImagePrint", CPUASendImagePrint); libraryhook_hook_proc("CPUASendImagePrint2", CPUASendImagePrint2); libraryhook_hook_proc("CPUASetPrintParameter", CPUASetPrintParameter); libraryhook_hook_proc("CPUXInit", CPUXInit); libraryhook_hook_proc("CPUXSearchPrinters", CPUXSearchPrinters); libraryhook_enable(avs::game::DLL_INSTANCE); } }