Files
spice2x.github.io/src/spice2x/games/shared/printer.cpp
T
bicarus 311404f56b overlay: toast notifications (#704)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds toast notifications to the overlay.

Toast notifications are transient windows (shown for about 4 seconds).
These are drawn on top of the game even when the overlay is inactive
(i.e., when no window is open), and does not cause any input sink
behavior.

Add toasts for some common user-driven actions that could use a
notification, such as:

* screenshot
* card insert
* PIN macro
* API client connect/disconnect
* virtual printer
* screen resize toggle / scene switch

Add an option to change where the toasts are displayed, or to turn it
off entirely.

Add SDK function for it, and add it to the samples.

## Testing
Tested DDR (shown at bottom right, as is for most games) and RB (shown
top right by default).
2026-05-27 00:45:27 -07:00

637 lines
21 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 "overlay/notifications.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("printer", "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_misc("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;
}
if (avs::game::is_model({"KLP", "KFC", "NCG"})) {
// rotate image clockwise
log_misc("printer", "rotate clockwise...");
auto rotated = new uint8_t[image_width * image_height * 3];
for (int x = 0; x < image_width; x++) {
for (int y = 0; y < image_height; y++) {
int index1 = (y * image_width + x) * 3;
int index2 = (x * image_height + y) * 3;
rotated[index2 + 0] = image_data[index1 + 0];
rotated[index2 + 1] = image_data[index1 + 1];
rotated[index2 + 2] = image_data[index1 + 2];
}
}
delete[] image_data;
image_data = rotated;
std::swap(image_width, image_height);
} else {
// flip horizontally
log_misc("printer", "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_misc("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);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
} else {
log_warning("printer", "printer emulation failed to write image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Error,
fmt::format("Printer: failed to write {}", fileutils::basename(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);
}
}