mirror of
https://github.com/Olde-Skuul/spaceaceiigs.git
synced 2024-05-31 18:41:28 +00:00
7fd252644d
Added all video assets encoded as GIF files and included the packvideo tool that will compress the art into the IIgs native format. Binaries for Mac and PC are supplied
641 lines
16 KiB
C++
641 lines
16 KiB
C++
/***************************************
|
|
|
|
Tool to pre-process video data for Space Ace IIgs
|
|
|
|
Copyright (c) 1995-2015 by Rebecca Ann Heineman <becky@burgerbecky.com>
|
|
|
|
It is released under an MIT Open Source license. Please see LICENSE
|
|
for license details. Yes, you can use it in a
|
|
commercial title without paying anything, just give me a credit.
|
|
Please? It's not like I'm asking you for money!
|
|
|
|
***************************************/
|
|
|
|
#include "packvideo.h"
|
|
|
|
/***************************************
|
|
|
|
Convert the IIgs palette to RGBAWord8_t
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPalette(RGBAWord8_t *pOutput,const Word8 *pInput)
|
|
{
|
|
Word uIndex = 0;
|
|
do {
|
|
pOutput->m_uRed = Renderer::RGB4ToRGB8Table[pInput[1]&0xFU];
|
|
pOutput->m_uGreen = Renderer::RGB4ToRGB8Table[pInput[0]>>4U];
|
|
pOutput->m_uBlue = Renderer::RGB4ToRGB8Table[pInput[0]&0xFU];
|
|
pOutput->m_uAlpha = 0xFF;
|
|
pInput+=2;
|
|
++pOutput;
|
|
} while (++uIndex<16);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert the RGBAWord8_t palette to IIgs
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPalette(Word8 *pOutput,const RGBAWord8_t *pInput)
|
|
{
|
|
Word uIndex = 0;
|
|
do {
|
|
Word uTemp = pInput->m_uGreen&0xF0U;
|
|
uTemp |= (pInput->m_uBlue>>4U);
|
|
pOutput[0] = static_cast<Word8>(uTemp);
|
|
pOutput[1] = static_cast<Word8>(pInput->m_uRed>>4U);
|
|
++pInput;
|
|
pOutput+=2;
|
|
} while (++uIndex<16);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Test if the IIgs palette has changed
|
|
|
|
***************************************/
|
|
|
|
static Word BURGER_API ComparePalette(const Word8 *pInput1,const Word8 *pInput2)
|
|
{
|
|
return MemoryCompare(pInput1,pInput2,32);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert bitmap to IIgs format
|
|
|
|
8 bits per pixel converted to 4 bits per pixel
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API ConvertPixelsToIIgs(Word8 *pOutput,const Image *pInput)
|
|
{
|
|
WordPtr uStride = pInput->GetStride()-320;
|
|
const Word8 *pPixels = pInput->GetImage();
|
|
WordPtr j=200;
|
|
do {
|
|
WordPtr i=320/2;
|
|
do {
|
|
Word uTemp = pPixels[0]<<4U;
|
|
uTemp |= (pPixels[1]&0xFU);
|
|
pOutput[0] = static_cast<Word8>(uTemp);
|
|
pPixels+=2;
|
|
++pOutput;
|
|
} while (--i);
|
|
pPixels+=uStride;
|
|
} while (--j);
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Compress a IIgs keyframe
|
|
|
|
The compression only packs runs of a minimum of 3 matching
|
|
bytes to reduce mode switching during decompression
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API CompressKeyFrame(OutputMemoryStream *pOutput,const Word8 *pInput)
|
|
{
|
|
// Number of bytes to process
|
|
WordPtr uInputLength = 320*200/2;
|
|
|
|
do {
|
|
// If two bytes or less, just store and exit
|
|
if (uInputLength<3) {
|
|
pOutput->Append(static_cast<Word8>(uInputLength));
|
|
pOutput->Append(pInput,uInputLength);
|
|
break;
|
|
}
|
|
|
|
// Check for repeater of at LEAST three bytes
|
|
Word uMatchTest = pInput[0];
|
|
|
|
// Is there a run?
|
|
if ((pInput[1] == uMatchTest) && (pInput[2] == uMatchTest)) {
|
|
|
|
// Maximum length of a matched run
|
|
WordPtr uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength; // 1-127
|
|
}
|
|
WordPtr uRun = 3-1;
|
|
while (++uRun<uMaximumRun) {
|
|
// Find end of repeater
|
|
if (pInput[uRun]!=uMatchTest) {
|
|
break;
|
|
}
|
|
}
|
|
// Encode 128-255 for 0-127 run
|
|
pOutput->Append(static_cast<Word8>(0x80|uRun));
|
|
pOutput->Append(static_cast<Word8>(uMatchTest));
|
|
uInputLength-=uRun;
|
|
pInput += uRun;
|
|
} else {
|
|
// Raw run, minimum size of 2 bytes
|
|
WordPtr uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
// Preload the next byte
|
|
uMatchTest = pInput[1];
|
|
WordPtr uRun = 2-1;
|
|
while (++uRun<uMaximumRun) {
|
|
// Scan for next repeater
|
|
if (pInput[uRun]==uMatchTest && (pInput[uRun+1]==uMatchTest)) {
|
|
// Remove from the run
|
|
--uRun;
|
|
break;
|
|
}
|
|
// Get the next byte
|
|
uMatchTest = pInput[uRun];
|
|
}
|
|
// Perform a raw data transfer
|
|
// Run 1-128
|
|
// Encode 0-127
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pOutput->Append(pInput,uRun);
|
|
uInputLength-=uRun;
|
|
pInput += uRun;
|
|
}
|
|
} while (uInputLength);
|
|
|
|
// Mark the end of compressed data
|
|
pOutput->Append(static_cast<Word8>(0));
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Compress a IIgs animation frame
|
|
|
|
***************************************/
|
|
|
|
static void BURGER_API CompressAnimFrame(OutputMemoryStream *pOutput,const Word8 *pPreviousFrame,const Word8 *pCurrentFrame)
|
|
{
|
|
// Number of bytes to process
|
|
WordPtr uInputLength = 320*200/2;
|
|
do {
|
|
// Check if there were any differences between the frames to create a skip token
|
|
WordPtr uMaximumRun = 127; // Skip token maximum value
|
|
if (uInputLength<uMaximumRun) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
|
|
// Test from the previous frame to the current frame
|
|
WordPtr uRun = 0;
|
|
do {
|
|
if (pPreviousFrame[uRun]!=pCurrentFrame[uRun]) {
|
|
break;
|
|
}
|
|
} while (++uRun<uMaximumRun);
|
|
|
|
// If the run is at least 2 bytes or end of the data, use it as is
|
|
|
|
if ((uRun==uInputLength) || (uRun>=3)) {
|
|
|
|
// Output a "skip" data token
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pPreviousFrame+=uRun;
|
|
pCurrentFrame+=uRun;
|
|
uInputLength-=uRun;
|
|
|
|
} else {
|
|
|
|
// Maximum length of a matched run
|
|
uMaximumRun = 255;
|
|
if (uInputLength<255) {
|
|
uMaximumRun = uInputLength; // 1-255
|
|
}
|
|
|
|
Word uMatchTest = pCurrentFrame[0];
|
|
uRun = 1;
|
|
while (uRun<uMaximumRun) {
|
|
// Find end of repeater
|
|
if (pCurrentFrame[uRun]!=uMatchTest) {
|
|
break;
|
|
}
|
|
++uRun;
|
|
}
|
|
|
|
// Is there a run of 4 or greater?
|
|
if (uRun>=4) {
|
|
// Encode the run length
|
|
pOutput->Append(static_cast<Word8>(0));
|
|
pOutput->Append(static_cast<Word8>(uRun));
|
|
pOutput->Append(static_cast<Word8>(uMatchTest));
|
|
uInputLength -= uRun;
|
|
pCurrentFrame += uRun;
|
|
pPreviousFrame += uRun;
|
|
|
|
} else {
|
|
|
|
// Raw run
|
|
uMaximumRun = 127;
|
|
if (uInputLength<127) {
|
|
uMaximumRun = uInputLength;
|
|
}
|
|
uRun = 0;
|
|
while (++uRun<uMaximumRun) {
|
|
// Scan for next repeater
|
|
if (pCurrentFrame[uRun]==uMatchTest && (pCurrentFrame[uRun+1]==uMatchTest) && (pCurrentFrame[uRun+2]==uMatchTest)) {
|
|
// Remove from the run
|
|
--uRun;
|
|
break;
|
|
}
|
|
if ((pCurrentFrame[uRun]==pPreviousFrame[uRun]) &&
|
|
(pCurrentFrame[uRun+1]==pPreviousFrame[uRun+1]) &&
|
|
(pCurrentFrame[uRun+2]==pPreviousFrame[uRun+2])) {
|
|
break;
|
|
}
|
|
// Get the next byte
|
|
uMatchTest = pCurrentFrame[uRun];
|
|
}
|
|
|
|
// Handle some data optimizations
|
|
|
|
// If it's only a single byte run and it's the same as the previous
|
|
// frame? Just skip
|
|
if ((uRun==1) && (pCurrentFrame[0]==pPreviousFrame[0])) {
|
|
pOutput->Append(static_cast<Word8>(1));
|
|
--uInputLength;
|
|
++pCurrentFrame;
|
|
++pPreviousFrame;
|
|
|
|
// If it's only a single byte run and it's the same as the previous
|
|
// frame? Just skip
|
|
} else if ((uRun==2) &&
|
|
(pCurrentFrame[0]==pPreviousFrame[0]) &&
|
|
(pCurrentFrame[1]==pPreviousFrame[1])) {
|
|
pOutput->Append(static_cast<Word8>(2));
|
|
uInputLength-=2;
|
|
pCurrentFrame+=2;
|
|
pPreviousFrame+=2;
|
|
|
|
} else {
|
|
// Perform a raw data transfer
|
|
// Run 1-128
|
|
pOutput->Append(static_cast<Word8>(0x80|uRun));
|
|
pOutput->Append(pCurrentFrame,uRun);
|
|
uInputLength-=uRun;
|
|
pCurrentFrame += uRun;
|
|
pPreviousFrame+=uRun;
|
|
}
|
|
}
|
|
}
|
|
} while (uInputLength>=2);
|
|
|
|
// Simple check if there's only one byte left
|
|
|
|
if (uInputLength==1) {
|
|
if (pCurrentFrame[0]==pPreviousFrame[0]) {
|
|
pOutput->Append(static_cast<Word8>(1));
|
|
} else {
|
|
pOutput->Append(static_cast<Word8>(0x81));
|
|
pOutput->Append(pCurrentFrame[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***************************************
|
|
|
|
Process a video file into space ace format
|
|
|
|
***************************************/
|
|
|
|
static Word ExtractVideo(OutputMemoryStream *pOutput,const Word8 *pInput,WordPtr uInputLength)
|
|
{
|
|
InputMemoryStream InputMem(pInput,uInputLength,TRUE);
|
|
Image MyImage;
|
|
FileGIF Giffy;
|
|
Word8 IIgsPalette[32];
|
|
Word8 NewIIgsPalette[32];
|
|
Word uResult = 10;
|
|
if (!Giffy.Load(&MyImage,&InputMem)) {
|
|
|
|
if ((MyImage.GetWidth()!=320) || (MyImage.GetHeight()!=200)) {
|
|
printf("Input file is not 320 x 200");
|
|
} else {
|
|
// Initialize the IIgs palette to invalid values
|
|
MemoryFill(IIgsPalette,255,sizeof(IIgsPalette));
|
|
Word8 *pCurrentFrame = static_cast<Word8 *>(Alloc(320*200/2));
|
|
Word8 *pPreviousFrame = static_cast<Word8 *>(Alloc(320*200/2));
|
|
int i = 1;
|
|
do {
|
|
|
|
// Process a frame
|
|
|
|
// Save space for the chunk size
|
|
WordPtr uOutputMark = pOutput->GetSize();
|
|
pOutput->Append(static_cast<Word16>(0));
|
|
|
|
// Convert the palette to IIgs format
|
|
ConvertPalette(NewIIgsPalette,Giffy.GetPalette());
|
|
|
|
// Set the default chunk type
|
|
|
|
Word8 uTypeFlag = 0x01;
|
|
|
|
// Is there a palette update?
|
|
if (ComparePalette(NewIIgsPalette,IIgsPalette)) {
|
|
MemoryCopy(IIgsPalette,NewIIgsPalette,sizeof(IIgsPalette));
|
|
uTypeFlag |= 0x80U;
|
|
}
|
|
|
|
// Initial frame?
|
|
if (i==1) {
|
|
uTypeFlag |= 0x60;
|
|
}
|
|
|
|
// Send the data type byte
|
|
pOutput->Append(static_cast<Word8>(uTypeFlag));
|
|
|
|
if (uTypeFlag&0x80U) {
|
|
pOutput->Append(IIgsPalette,32);
|
|
}
|
|
|
|
ConvertPixelsToIIgs(pCurrentFrame,&MyImage);
|
|
|
|
if (uTypeFlag&0x40) {
|
|
CompressKeyFrame(pOutput,pCurrentFrame);
|
|
} else {
|
|
CompressAnimFrame(pOutput,pPreviousFrame,pCurrentFrame);
|
|
}
|
|
MemoryCopy(pPreviousFrame,pCurrentFrame,320*200/2);
|
|
|
|
// Update the chunk size
|
|
Word16 uChuckShort;
|
|
LittleEndian::Store(&uChuckShort,static_cast<Word16>(pOutput->GetSize()-uOutputMark));
|
|
pOutput->Overwrite(&uChuckShort,2,uOutputMark);
|
|
++i;
|
|
} while (!Giffy.LoadNextFrame(&MyImage,&InputMem));
|
|
Free(pCurrentFrame);
|
|
Free(pPreviousFrame);
|
|
// Append an "End of data" marker
|
|
pOutput->Append(static_cast<Word16>(0xFF00U));
|
|
uResult = 0;
|
|
}
|
|
} else {
|
|
printf("Gif input file error!\n");
|
|
}
|
|
return uResult;
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Convert a Space Ace file to an animated GIF file
|
|
|
|
***************************************/
|
|
|
|
static char Name[] = "filexxx.gif";
|
|
|
|
static Word EncapsulateToGIF(OutputMemoryStream *pOutput,Filename *pOutputFilename,const Word8 *pInput,WordPtr uInputLength)
|
|
{
|
|
// Too small?
|
|
if (uInputLength<2) {
|
|
return 10;
|
|
}
|
|
|
|
FileGIF GIF;
|
|
Image MyImage;
|
|
|
|
// Create an initial image
|
|
MyImage.Init(320,200,Image::PIXELTYPE8BIT);
|
|
MyImage.ClearBitmap();
|
|
MemoryClear(GIF.GetPalette(),sizeof(GIF.GetPalette()[0])*256);
|
|
|
|
//
|
|
// Decompress a chunk
|
|
//
|
|
|
|
Word uFrame = 0;
|
|
for (;;) {
|
|
Word uChunkSize = LittleEndian::LoadAny(reinterpret_cast<const Word16 *>(pInput));
|
|
if (uChunkSize>=0xFF00) {
|
|
printf("End of data, frames = %u\n",uFrame);
|
|
break;
|
|
}
|
|
++uFrame;
|
|
if (uChunkSize>uInputLength) {
|
|
printf("Premature end of data\n");
|
|
return 10;
|
|
}
|
|
if (uChunkSize<2) {
|
|
printf("Chunk size too small\n");
|
|
return 10;
|
|
}
|
|
printf("Chunk is %u bytes\n",uChunkSize);
|
|
const Word8 *pWork = pInput+2;
|
|
uInputLength -= uChunkSize;
|
|
pInput+= uChunkSize;
|
|
uChunkSize-=2;
|
|
|
|
// Get the palette token
|
|
|
|
if (uChunkSize) {
|
|
Word uType = pWork[0];
|
|
++pWork;
|
|
--uChunkSize;
|
|
printf("Token = 0x%02X\n",uType);
|
|
|
|
if (uType&0x80) {
|
|
|
|
// Clear out the palette
|
|
MemoryClear(GIF.GetPalette(),sizeof(GIF.GetPalette()[0])*256);
|
|
ConvertPalette(GIF.GetPalette(),pWork);
|
|
pWork+=32;
|
|
uChunkSize-=32;
|
|
}
|
|
|
|
// Full image or animation frame?
|
|
if (uType&0x40) {
|
|
Word uTemp;
|
|
Word8 *pDest = MyImage.GetImage();
|
|
|
|
for (;;) {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (!uTemp) {
|
|
break;
|
|
}
|
|
if (uTemp&0x80) {
|
|
uTemp&=0x7f;
|
|
if (uTemp) {
|
|
// Run length compressed loop
|
|
Word uSecond = pWork[0];
|
|
++pWork;
|
|
Word uFirst = uSecond>>4U;
|
|
uSecond&=0xF;
|
|
do {
|
|
pDest[0] = static_cast<Word8>(uFirst);
|
|
pDest[1] = static_cast<Word8>(uSecond);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else {
|
|
// Uncompressed loop
|
|
do {
|
|
Word uColor = pWork[0];
|
|
++pWork;
|
|
pDest[0] = static_cast<Word8>(uColor>>4U);
|
|
pDest[1] = static_cast<Word8>(uColor&0xF);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
}
|
|
} else {
|
|
|
|
|
|
Word uTemp;
|
|
Word8 *pDest = MyImage.GetImage();
|
|
Word8 *pEnd = pDest+(320*200);
|
|
do {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (!uTemp) {
|
|
uTemp = pWork[0];
|
|
++pWork;
|
|
if (uTemp) {
|
|
// Run length compressed loop
|
|
Word uSecond = pWork[0];
|
|
++pWork;
|
|
Word uFirst = uSecond>>4U;
|
|
uSecond&=0xF;
|
|
do {
|
|
pDest[0] = static_cast<Word8>(uFirst);
|
|
pDest[1] = static_cast<Word8>(uSecond);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else if (uTemp&0x80) {
|
|
uTemp&=0x7f;
|
|
if (uTemp) {
|
|
// Uncompressed loop
|
|
do {
|
|
Word uColor = pWork[0];
|
|
++pWork;
|
|
pDest[0] = static_cast<Word8>(uColor>>4U);
|
|
pDest[1] = static_cast<Word8>(uColor&0xF);
|
|
pDest+=2;
|
|
} while (--uTemp);
|
|
}
|
|
} else {
|
|
pDest+=(uTemp*2);
|
|
}
|
|
} while (pDest<pEnd);
|
|
}
|
|
}
|
|
GIF.Save(pOutput,&MyImage);
|
|
Filename TempName(pOutputFilename[0]);
|
|
TempName.SetFileExtension(NULL);
|
|
NumberString Namex(static_cast<Word32>(uFrame),LEADINGZEROS|3);
|
|
String Name2(TempName.GetPtr());
|
|
Name2.Remove(Name2.GetLength()-1);
|
|
Name2.Append(Namex,3);
|
|
TempName.Set(Name2.GetPtr());
|
|
TempName.SetFileExtension("gif");
|
|
printf("Frame %s\n",TempName.GetPtr());
|
|
pOutput->SaveFile(&TempName);
|
|
pOutput->Clear();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/***************************************
|
|
|
|
Main dispatcher
|
|
|
|
***************************************/
|
|
|
|
int BURGER_ANSIAPI main(int argc,const char **argv)
|
|
{
|
|
ConsoleApp MyApp(argc,argv);
|
|
CommandParameterBooleanTrue DoVideo("Process Video","v");
|
|
CommandParameterBooleanTrue ConvertToGIF("Convert to GIF","g");
|
|
const CommandParameter *MyParms[] = {
|
|
&DoVideo,
|
|
&ConvertToGIF
|
|
};
|
|
#if 0
|
|
Filename DeathName;
|
|
DeathName.SetFromNative("D:\\projects\\burger\\games\\spaceace\\iigs\\assets\\death\\death07.gif");
|
|
FileGIF Giffy;
|
|
InputMemoryStream InputMem;
|
|
if (!InputMem.Open(&DeathName)) {
|
|
Image MyImage;
|
|
if (!Giffy.Load(&MyImage,&InputMem)) {
|
|
OutputMemoryStream OutputMem;
|
|
int i = 1;
|
|
do {
|
|
OutputMem.Clear();
|
|
Giffy.Save(&OutputMem,&MyImage);
|
|
char name[256];
|
|
sprintf(name,"D:\\projects\\burger\\games\\spaceace\\iigs\\assets\\death\\death07x%d.gif",i);
|
|
DeathName.SetFromNative(name);
|
|
OutputMem.SaveFile(&DeathName);
|
|
++i;
|
|
} while (!Giffy.LoadNextFrame(&MyImage,&InputMem));
|
|
}
|
|
}
|
|
#endif
|
|
argc = MyApp.GetArgc();
|
|
argv = MyApp.GetArgv();
|
|
argc = CommandParameter::Process(argc,argv,MyParms,sizeof(MyParms)/sizeof(MyParms[0]),
|
|
"Usage: packvideo InputFile OutputFile\n\n"
|
|
"Preprocess video data for Space Ace IIgs.\nCopyright by Rebecca Ann Heineman\n",3);
|
|
if (argc<0) {
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
MyApp.SetArgc(argc);
|
|
|
|
Filename InputName;
|
|
InputName.SetFromNative(argv[1]);
|
|
|
|
WordPtr uInputLength;
|
|
Word8 *pInput = static_cast<Word8 *>(FileManager::LoadFile(&InputName,&uInputLength));
|
|
if (!pInput) {
|
|
printf("Can't open %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
|
|
// Convert gif to data
|
|
if (DoVideo.GetValue()) {
|
|
OutputMemoryStream Output;
|
|
if (ExtractVideo(&Output,pInput,uInputLength)) {
|
|
printf("Can't convert %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
Filename OutputName;
|
|
OutputName.SetFromNative(argv[2]);
|
|
if (Output.SaveFile(&OutputName)) {
|
|
printf("Can't save %s!\n",argv[2]);
|
|
Globals::SetErrorCode(10);
|
|
}
|
|
}
|
|
|
|
// Convert raw video to GIF
|
|
} else if (ConvertToGIF.GetValue()) {
|
|
Filename OutputName;
|
|
OutputName.SetFromNative(argv[2]);
|
|
OutputMemoryStream Output;
|
|
if (EncapsulateToGIF(&Output,&OutputName,pInput,uInputLength)) {
|
|
printf("Can't convert %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
} else {
|
|
// if (Output.SaveFile(&OutputName)) {
|
|
// printf("Can't save %s!\n",argv[2]);
|
|
// Globals::SetErrorCode(10);
|
|
// }
|
|
}
|
|
} else {
|
|
printf("No conversion selected for %s!\n",argv[1]);
|
|
Globals::SetErrorCode(10);
|
|
}
|
|
Free(pInput);
|
|
}
|
|
}
|
|
return Globals::GetErrorCode();
|
|
}
|