spicetools/games/shared/printer.cpp

610 lines
20 KiB
C++

#include "printer.h"
#include <vector>
#include <thread>
#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<std::string> PRINTER_PATH;
std::vector<std::string> 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);
}
}