| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 | #include "IDScanner.h"#include "main.h"#include "../winamp/wa_ipc.h"#include "api__ml_plg.h"#include "playlist.h"#include <assert.h>#include <atlbase.h>#include <strsafe.h>	// include this last//#define DEBUG_CALLBACKSIDScanner::IDScanner() : systemCallbacks(0){	musicID=0;	killswitch=0;	filesComplete=0;	filesTotal=0;	state=STATE_IDLE;	m_dwCookie=0;	syscb_registered=false;	// Create the stack that will hold our batched up files for step 4 processing	//process_items;}IDScanner::~IDScanner(){	// ToDo: Make sure we clean up the processing stack here if we need to do that	//Shutdown();}void IDScanner::Shutdown(){	if (musicID)	{		IConnectionPoint *icp = GetConnectionPoint(musicID, DIID__ICDDBMusicIDManagerEvents);		if (icp)		{			icp->Unadvise(m_dwCookie);			icp->Release(); 		}		musicID->Shutdown();		musicID->Release();	}	musicID=0;	// Deregister the system callbacks	WASABI_API_SYSCB->syscb_deregisterCallback(this);}static HRESULT FillTag(ICddbFileInfo *info, BSTR filename){	ICddbID3TagPtr infotag = NULL;	infotag.CreateInstance(CLSID_CddbID3Tag);	ICddbFileTag2_5Ptr tag2_5 = NULL;	infotag->QueryInterface(&tag2_5);	itemRecordW *record = AGAVE_API_MLDB->GetFile(filename);	if (record && infotag && tag2_5)	{		wchar_t itemp[64] = {0};		if (record->artist)			infotag->put_LeadArtist(record->artist);		if (record->album)			infotag->put_Album(record->album);		if (record->title)			infotag->put_Title(record->title);		if (record->genre)			infotag->put_Genre(record->genre);		if (record->track > 0)			infotag->put_TrackPosition(_itow(record->track, itemp, 10));// TODO:	if (record->tracks > 0)		if (record->year > 0)			infotag->put_Year(_itow(record->year, itemp, 10));		if (record->publisher)			infotag->put_Label(record->publisher);		/*		if (GetFileInfo(filename, L"ISRC", meta, 512) && meta[0])			infotag->put_ISRC(meta);		*/		if (record->disc > 0)			infotag->put_PartOfSet(_itow(record->disc, itemp, 10));		if (record->albumartist)			tag2_5->put_DiscArtist(record->albumartist);		if (record->composer)			tag2_5->put_Composer(record->composer);		if (record->length > 0)			tag2_5->put_LengthMS(_itow(record->length*1000, itemp, 10));		if (record->bpm > 0)			infotag->put_BeatsPerMinute(_itow(record->bpm, itemp, 10));		/*		if (GetFileInfo(filename, L"conductor", meta, 512) && meta[0])					tag2_5->put_Conductor(meta);					*/		AGAVE_API_MLDB->FreeRecord(record);	}	if (info) info->put_Tag(infotag);	return S_OK;}void IDScanner::CommitFileInfo(ICddbFileInfo *match){	ICddbFileTagPtr tag;	match->get_Tag(&tag);	ICddbDisc2Ptr disc1, disc;	match->get_Disc(&disc1);	ICddbDisc2_5Ptr disc2_5;	ICddbTrackPtr track;	ICddbTrack2_5Ptr track2;	if (disc1)	{		musicID->GetFullDisc(disc1, &disc);		if (disc == 0)			disc=disc1;		disc->QueryInterface(&disc2_5);		disc->GetTrack(1, &track);		if (track)			track->QueryInterface(&track2);	}	CComBSTR file, tagID, extData;	match->get_Filename(&file);	tag->get_FileId(&tagID);	playlistMgr->FileSetTagID(file, tagID, CDDB_UPDATE_NONE);	ICddbFileTag2_5Ptr tag2;	tag->QueryInterface(&tag2);	playlistMgr->FileSetFieldVal(file, gnpl_crit_field_xdev1, L"0"); // mark as done!	if (tag2) // try tag first		tag2->get_ExtDataSerialized(&extData);	if (!extData && track2 != 0)  // WMA files don't get their tag object's extended data set correctly, so fallback to track extended data		track2->get_ExtDataSerialized(&extData);	if (!extData && disc2_5 != 0) // finally, fall back to disc extended data		disc2_5->get_ExtDataSerialized(&extData);	playlistMgr->FileSetExtDataSerialized(file, extData, CDDB_UPDATE_NONE);	if (tagID)		AGAVE_API_MLDB->SetField(file, "GracenoteFileID", tagID);	if (extData)		AGAVE_API_MLDB->SetField(file, "GracenoteExtData", extData);	// TODO: if we don't have an artist & album, we might as well grab this out of the tag now	// TODO: make thread-safe and optional	/*	updateFileInfo(file, L"GracenoteFileID", tagID);	updateFileInfo(file, L"GracenoteExtData", extData);	WriteFileInfo(file);	*/}STDMETHODIMP STDMETHODCALLTYPE IDScanner::QueryInterface(REFIID riid, PVOID *ppvObject){	if (!ppvObject)		return E_POINTER;	else if (IsEqualIID(riid, __uuidof(_ICDDBMusicIDManagerEvents)))		*ppvObject = (_ICDDBMusicIDManagerEvents *)this;	else if (IsEqualIID(riid, IID_IDispatch))		*ppvObject = (IDispatch *)this;	else if (IsEqualIID(riid, IID_IUnknown))		*ppvObject = this;	else	{		*ppvObject = NULL;		return E_NOINTERFACE;	}	AddRef();	return S_OK;}ULONG STDMETHODCALLTYPE IDScanner::AddRef(void){	return 1;}ULONG STDMETHODCALLTYPE IDScanner::Release(void){	return 0;}HRESULT STDMETHODCALLTYPE IDScanner::Invoke(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, EXCEPINFO FAR * pexecinfo, unsigned int FAR *puArgErr){	switch (dispid)	{		case 1: // OnTrackIDStatusUpdate, params: CddbMusicIDStatus Status, BSTR filename, long* Abort		{			//long *abort = pdispparams->rgvarg[0].plVal;			// TODO: is this safe to put here?  Or does this make us get partial results		}		break;		case 2: // OnAlbumIDStatusUpdate, params: CddbMusicIDStatus Status, BSTR filename, long current_file, long total_files, long* Abort		{			long *abort = pdispparams->rgvarg[0].plVal;			/*long total_files = pdispparams->rgvarg[1].lVal;			long current_file= pdispparams->rgvarg[2].lVal;*/			CddbMusicIDStatus status = (CddbMusicIDStatus)pdispparams->rgvarg[4].lVal;			BSTR filename = pdispparams->rgvarg[3].bstrVal;			// TODO: is this safe to put here?  Or does this make us get partial results			if (killswitch)				*abort = 1;		}		break;		case 3: // OnTrackIDComplete, params: LONG match_code, ICddbFileInfo* pInfoIn, ICddbFileInfoList* pListOut			break;		case 4:			break;// OnAlbumIDComplete, params: LONG match_code, ICddbFileInfoList* pListIn, ICddbFileInfoLists* pListsOut		case 5:			break; // OnGetFingerprint		case 6:			break;		case 7://OnLibraryIDListStarted			break;		case 8: // OnLibraryIDListComplete		{			long *abort  = pdispparams->rgvarg[0].plVal;			if (killswitch)				*abort = 1;			/*long FilesError =pdispparams->rgvarg[1].lVal;			long FilesNoMatch=pdispparams->rgvarg[2].lVal;			long FilesFuzzy=pdispparams->rgvarg[3].lVal;			long FilesExact=pdispparams->rgvarg[4].lVal;*/			filesTotal=pdispparams->rgvarg[5].lVal;			filesComplete=pdispparams->rgvarg[6].lVal;			IDispatch *disp = pdispparams->rgvarg[7].pdispVal;			if (disp)			{				ICddbFileInfoList* matchList=0;				disp->QueryInterface(&matchList);				if (matchList)				{					long matchcount;					matchList->get_Count(&matchcount);					for (int j = 1;j <= matchcount;j++)					{						ICddbFileInfoPtr match;						matchList->GetFileInfo(j, &match);						CommitFileInfo(match);					}					matchList->Release();				}				return S_OK;			}			else				return E_FAIL;		}		break;		case 9: //OnLibraryIDComplete			break;		case 10: // OnGetFingerprintInfo		{			long *abort = pdispparams->rgvarg[0].plVal;			IDispatch *disp = pdispparams->rgvarg[1].pdispVal;			BSTR filename = pdispparams->rgvarg[2].bstrVal;			ICddbFileInfoPtr info;			disp->QueryInterface(&info);			return AGAVE_API_GRACENOTE->CreateFingerprint(musicID, AGAVE_API_DECODE, info, filename, abort);		}		break;		case 11: // OnGetTagInfo		{			pdispparams->rgvarg[0].plVal;			IDispatch *disp = pdispparams->rgvarg[1].pdispVal;			BSTR filename = pdispparams->rgvarg[2].bstrVal;			ICddbFileInfoPtr info;			disp->QueryInterface(&info);			return FillTag(info, filename);		}		break;	}	return DISP_E_MEMBERNOTFOUND;}HRESULT STDMETHODCALLTYPE IDScanner::GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgdispid){	*rgdispid = DISPID_UNKNOWN;	return DISP_E_UNKNOWNNAME;}HRESULT STDMETHODCALLTYPE IDScanner::GetTypeInfo(unsigned int itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo){	return E_NOTIMPL;}HRESULT STDMETHODCALLTYPE IDScanner::GetTypeInfoCount(unsigned int FAR * pctinfo){	return E_NOTIMPL;}void IDScanner::SetGracenoteData(BSTR filename, BSTR tagID, BSTR extData){	bool foundExt=false;	if (extData && extData[0])	{		playlistMgr->FileSetExtDataSerialized(filename, extData, CDDB_UPDATE_NONE);		CComBSTR test;		playlistMgr->FileGetExtDataSerialized(filename, &test, 0); // benski> 24 Jul 2007 - there is a currently a bug that makes this always E_FAIL		if (test)			foundExt=true;	}	if (!foundExt) // no Extended Data (or invalid), but we have a Tag ID, we'll ask the playlist SDK to do a quick lookup	{		playlistMgr->FileSetTagID(filename, tagID, CDDB_UPDATE_EXTENDED);		// write back to Media Library database		CComBSTR extData;		playlistMgr->FileGetExtDataSerialized(filename, &extData, 0); // benski> 24 Jul 2007 - there is a currently a bug that makes this always E_FAIL		if (extData)			AGAVE_API_MLDB->SetField(filename, "GracenoteExtData", extData);	}	else		playlistMgr->FileSetTagID(filename, tagID, CDDB_UPDATE_NONE);}/*//void IDScanner::ProcessDatabaseDifferences(Device * dev, C_ItemList * ml0,C_ItemList * itemRecordsOnDevice, C_ItemList * itemRecordsNotOnDevice, C_ItemList * songsInML,  C_ItemList * songsNotInML)void IDScanner::ProcessDatabaseDifferences(Device * dev, C_ItemList * ml0,C_ItemList * itemRecordsOnDevice, C_ItemList * itemRecordsNotOnDevice, C_ItemList * songsInML,  C_ItemList * songsNotInML){  C_ItemList device2;	C_ItemList *device0=&device2;   int l = dev->getPlaylistLength(0);   for(int i=0; i<l; i++) device0->Add((void*)dev->getPlaylistTrack(0,i));  qsort(ml0->GetAll(),ml0->GetSize(),sizeof(void*),sortfunc_ItemRecords);	nu::qsort(device0->GetAll(), device0->GetSize(), sizeof(void*), dev, compareSongs);  C_ItemList *ml = new C_ItemList;  C_ItemList *device = new C_ItemList;    int i,j;    {    itemRecordW * lastice = NULL;    songid_t lastsong = NULL;    for(i=0; i<ml0->GetSize(); i++) {      itemRecordW * it = (itemRecordW*)ml0->Get(i);      if(lastice) if(compareItemRecords(lastice,it)==0) continue;      ml->Add(it);      lastice = it;    }    for(i=0; i<device0->GetSize(); i++) {      songid_t song = (songid_t)device0->Get(i);      if(lastsong) if(compareSongs((void*)&song,(void*)&lastsong, dev)==0) continue;      device->Add((void*)song);      lastsong = song;    }  }  i=0,j=0;  int li = device->GetSize();  int lj = ml->GetSize();  while(i<li && j<lj) {    itemRecordW * it = (itemRecordW*)ml->Get(j);    songid_t song = (songid_t)device->Get(i);        int cmp = compareItemRecordAndSongId(it,song, dev);    if(cmp == 0) { // song on both      if(itemRecordsOnDevice) itemRecordsOnDevice->Add(it);      if(songsInML) songsInML->Add((void*)song);      i++;      j++;    }    else if(cmp > 0) { //song in ml and not on device      if(itemRecordsNotOnDevice) itemRecordsNotOnDevice->Add(it);      j++;    }    else { // song on device but not in ML      if(songsNotInML) songsNotInML->Add((void*)song);      i++;    }  }  // any leftovers?  if(songsNotInML) while(i<li) {     songid_t song = (songid_t)device->Get(i++);        songsNotInML->Add((void*)song);   }  if(itemRecordsNotOnDevice) while(j<lj) {    itemRecordW * it = (itemRecordW *)ml->Get(j++);        itemRecordsNotOnDevice->Add(it);   }  delete ml; delete device;}*//*2-pass strategyPass 1:  Find all tracks with Gracenote Extended DataPass 2:  Find File ID & extended data by fingerprint*/void IDScanner::ScanDatabase(){	filesComplete=0;	filesTotal=0;	state=STATE_INITIALIZING;	killswitch=0; // reset just in case	if (SetupPlaylistSDK())	{		// If this is our first time running then lets register the wasabi system callbacks for adding and removing tracks		if (!syscb_registered)		{			WASABI_API_SYSCB->syscb_registerCallback(this);			syscb_registered = true;		}		// Set up the MLDB manager		InitializeMLDBManager();		state=STATE_SYNC;		/* Get a list of files in the media library database */		itemRecordListW *results = AGAVE_API_MLDB->Query(L"type=0");		if (results)		{			filesTotal=results->Size;			for (int i=0;i<results->Size;i++)			{				if (killswitch)					break;				wchar_t * filename = results->Items[i].filename;				HRESULT hr=playlistMgr->AddEntry(filename);					// Add entry to gracenote DB				assert(SUCCEEDED(S_OK));				if (hr == S_OK)				{					// Fill in Artist & Album info since we have it in the itemRecordList anyway					// TODO: probably want to use SKIP_THE_AND_WHITESPACE here					if (results->Items[i].album && results->Items[i].album[0])						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_album_name, results->Items[i].album);					if (results->Items[i].artist && results->Items[i].artist[0])						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_track_artist_name, results->Items[i].artist);					// Populate title information so that we have more complete data.					if (results->Items[i].title && results->Items[i].title[0])						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_track_name, results->Items[i].title);					wchar_t storage[64] = {0};					// Populate the file length in milliseconds					if (results->Items[i].length > 0)					{						_itow(results->Items[i].length,storage, 10);						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_file_length, storage);					}					// Populate the file size in kilobytes					if (results->Items[i].filesize > 0)					{						_itow(results->Items[i].filesize,storage, 10);						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_file_size, storage);					}					wchar_t *tagID = getRecordExtendedItem(&results->Items[i], L"GracenoteFileID");					if (tagID && tagID[0])					{						SetGracenoteData(filename, tagID, getRecordExtendedItem(&results->Items[i], L"GracenoteExtData"));						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_xdev1, L"0"); // done with this file!					}					else						hr=playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_xdev1, L"1"); // move to phase 1				}				filesComplete=i+1;			}			AGAVE_API_MLDB->FreeRecordList(results);			state=STATE_METADATA;			filesComplete=0;			if (!killswitch)				Pass1();			filesComplete=0;			state=STATE_MUSICID;			if (!killswitch)				Pass2();			state=STATE_DONE;			if (!killswitch)				AGAVE_API_MLDB->Sync();		}		// Set the pass 2 flag back so that on next generation we dont try to run it		run_pass2_flag = false;	}	else		state=STATE_ERROR;}bool IDScanner::GetStatus(long *pass, long *track, long *tracks){	*pass = state;	*track = filesComplete;	*tracks = filesTotal;	return true;}// System callback handlers from WASABIFOURCC IDScanner::GetEventType(){	return api_mldb::SYSCALLBACK;}int IDScanner::notify(int msg, intptr_t param1, intptr_t param2){	wchar_t *filename = (wchar_t *)param1;	switch (msg)	{		case api_mldb::MLDB_FILE_ADDED:		{			DebugCallbackMessage(param1, L"File Added: '%s'");			// Call the add/update function that needs to run on our lonesome playlist generator thread			WASABI_API_THREADPOOL->RunFunction(plg_thread, IDScanner::MLDBFileAddedOnThread, _wcsdup(filename), (intptr_t)this, api_threadpool::FLAG_REQUIRE_COM_STA);		}		break;		case api_mldb::MLDB_FILE_REMOVED_PRE:		{			// We are not concerned with the PRE scenario			//DebugCallbackMessage(param1, L"File Removed PRE: '%s'");		}		break;		case api_mldb::MLDB_FILE_REMOVED_POST:		{			WASABI_API_THREADPOOL->RunFunction(plg_thread, IDScanner::MLDBFileRemovedOnThread, _wcsdup(filename), (intptr_t)this, api_threadpool::FLAG_REQUIRE_COM_STA);			// We will only care about the post scenario since we just need to remove the file entry from gracenote.			//DebugCallbackMessage(param1, L"File Removed POST: '%s'");		}		break;		case api_mldb::MLDB_FILE_UPDATED:		{			// For now we call the add method even on an update			WASABI_API_THREADPOOL->RunFunction(plg_thread, IDScanner::MLDBFileAddedOnThread, _wcsdup(filename), (intptr_t)this, api_threadpool::FLAG_REQUIRE_COM_STA);			//DebugCallbackMessage(param1, L"File Updated: '%s'");		}		break;		case api_mldb::MLDB_CLEARED:		{			WASABI_API_THREADPOOL->RunFunction(plg_thread, IDScanner::MLDBClearedOnThread, 0, (intptr_t)this, api_threadpool::FLAG_REQUIRE_COM_STA);			//DebugCallbackMessage(param1, L"MLDB Cleared");		}		break;		default: return 0;	}	return 1;}// Outputs a messagebox with a filename to know when callbacks are being triggeredinline void IDScanner::DebugCallbackMessage(const intptr_t text, const wchar_t *message){//#ifdef DEBUG_CALLBACKS#if defined(DEBUG) && defined(DEBUG_CALLBACKS)	const int size = MAX_PATH + 256;	wchar_t *filename = (wchar_t *)text;	wchar_t buff[size] = {0};	//wsprintf(buff, size, message, filename);	StringCchPrintf(buff, size, message, filename);	MessageBox(0, buff, L"Wasabi Callback Debug", 0);#endif}int IDScanner::MLDBFileAddedOnThread(HANDLE handle, void *user_data, intptr_t id){	if (!playlistMgr) return 0;	// Variables to hold information about the file query	wchar_t *filename = (wchar_t *)user_data;	IDScanner *scanner = (IDScanner *)id;	wchar_t buff[1024] = {0};	_ltow(scanner->state, buff, 10);			itemRecordW *result = AGAVE_API_MLDB->GetFile(filename);		HRESULT hr=playlistMgr->AddEntry(filename);	// Add the file entry to the gracenote DB		assert(SUCCEEDED(S_OK));		if (hr == S_OK /*&& results->Size == 1*/)	{		// Fill in Artist & Album info since we have it in the itemRecordList anyway		// TODO: probably want to use SKIP_THE_AND_WHITESPACE here		if (result->album && result->album[0])			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_album_name, result->album);		if (result->artist && result->artist[0])			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_track_artist_name, result->artist);		// Populate title, file size, and length information so that we have more complete data.		if (result->title && result->title[0])			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_track_name, result->title);		wchar_t storage[64] = {0};		// Populate the file length in milliseconds		if (result->length > 0)		{			_itow(result->length,storage, 10);			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_file_length, storage);		}		// Populate the file size in kilobytes		if (result->filesize > 0)		{			_itow(result->filesize,storage, 10);			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_file_size, storage);		}		wchar_t *tagID = getRecordExtendedItem(result, L"GracenoteFileID");		if (tagID && tagID[0])		{			scanner->SetGracenoteData(filename, tagID, getRecordExtendedItem(result, L"GracenoteExtData"));			// We have everything we need at this point in the gracenote DB			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_xdev1, L"0"); // done with this file!		}		else		// Set it to the final scan 		{			playlistMgr->FileSetFieldVal(filename, gnpl_crit_field_xdev1, L"2"); // move to phase 2, we can skip phase 1 			// Add the current file to the step 4 processing stack			// TODOD is there a mem leak here??			ProcessItem *itemz = new ProcessItem();			itemz->filename = filename;			//scanner->process_items.push(*itemz);			// Add the current item coming in to the queue			// Set the flag so that we know we will need to rerun step 4 (pass 2) on a playlist regeneration, this only needs to happen if there is an actual change.			run_pass2_flag = true;		}	}	if (result)		AGAVE_API_MLDB->FreeRecord(result);	// ToDo: We need to do this free when we pop it off of the processing stack later	free(filename);	// Clean up the user data	return 0;}int IDScanner::MLDBFileRemovedOnThread(HANDLE handle, void *user_data, intptr_t id){	wchar_t *filename = (wchar_t *)user_data;	HRESULT hr = playlistMgr->DeleteFile(filename);	if (hr == S_OK)		return 0;	else		return 1;	free(filename);	// Clean up the user data}int IDScanner::MLDBClearedOnThread(HANDLE handle, void *user_data, intptr_t id){	return ResetDB(false);}int IDScanner::ProcessStackItems(void){	// ToDo: Run through the stack items and process stage 4 on them	//this->	return 1;}#define CBCLASS IDScannerSTART_DISPATCH;CB(SYSCALLBACK_GETEVENTTYPE, GetEventType);CB(SYSCALLBACK_NOTIFY, notify);END_DISPATCH;#undef CBCLASS
 |