Hi all!
So glad to receive your welcome.
Today, I've fixed the problem when using invoke wsprintf with many parameters (12), by passing params right-to-left, calling the function and cleaning up the stack by hand. :badgrin:
I hope to learn more from all of you.
Best regards,
Quote from: quocsan on March 18, 2025, 10:56:03 PMToday, I've fixed the problem when using invoke wsprintf with many parameters (12), by passing params right-to-left, calling the function and cleaning up the stack by hand. :badgrin:
Maybe you could teach us then. :tongue: just kidding.
:biggrin:
Happy coding! :thumbsup:
Quote from: quocsan on March 18, 2025, 10:56:03 PMI've fixed the problem when using invoke wsprintf with many parameters (12)
What was the problem? Normally, an invoke crt_wsprintf, .... should work without any problems :cool:
Quote from: jj2007 on March 19, 2025, 05:24:50 AMQuote from: quocsan on March 18, 2025, 10:56:03 PMI've fixed the problem when using invoke wsprintf with many parameters (12)
QuoteWhat was the problem? Normally, an invoke crt_wsprintf, .... should work without any problems :cool:
When I used invoke, I passed
edx
among other params. But invoke really seemed to push edx with value different from it was. So I had to make the call by hand.
xor edx, edx
mov eax, edx ; eax = edx = 0
mov dl, Header.m_UpdatedYY ; year of updated date
.if (dl < 80)
add dx, 2000
.else
add dx, 1900
.endif
mov ax, Header.m_RecordSize
push eax
mov ax, Header.m_HeaderSize
push eax
push Header.m_RecordCount
push edx
xor eax, eax
mov al, Header.m_UpdatedMM
push eax
mov al, Header.m_UpdatedDD
push eax
push DbfDesc
mov al, Header.m_ID
push eax
push FileSize
push offset fname
push offset szFmt
push offset szBuffer
call wsprintf
add esp, 4*12
Quote from: quocsan on March 19, 2025, 08:26:32 AMinvoke really seemed to push edx with value different from it was
Interesting: this should not happen. The only scenario I could imagine is that one of your arguments is a macro that changes edx. Could you please post a complete example where that problem happens? I'd like to see it under a debugger.
I second that emotion.
INVOKE does nothing to change any registers, except for EAX if one of the parameters happens to be a memory reference using ADDR, in which case you'll get this for that parameter:
LEA EAX, <parameter>
PUSH EAX
in which case you cannot use EAX anywhere to the right of that parameter (in the parameter list you give INVOKE). But it'll never mess with EDX, so you must have done something wrong.
Quote from: jj2007 on March 19, 2025, 08:29:27 AMQuote from: quocsan on March 19, 2025, 08:26:32 AMinvoke really seemed to push edx with value different from it was
Interesting: this should not happen. The only scenario I could imagine is that one of your arguments is a macro that changes edx. Could you please post a complete example where that problem happens? I'd like to see it under a debugger.
Thank you for your kindness. I shall take your suggestions and shall study the matter deeply when having time.
Well, this is really a utility I write to fix corrupted DBF files caused by sudden electricity cut. To test it, we have to use a company's private DBF files. :biggrin:
Quote from: NoCforMe on March 19, 2025, 08:38:00 AMI second that emotion.
INVOKE does nothing to change any registers, except for EAX if one of the parameters happens to be a memory reference using ADDR, in which case you'll get this for that parameter:
LEA EAX, <parameter>
PUSH EAX
in which case you cannot use EAX anywhere to the right of that parameter (in the parameter list you give INVOKE). But it'll never mess with EDX, so you must have done something wrong.
Thank you for your notes. I shall pay more attention when using params with
invoke.
Quote from: quocsan on March 19, 2025, 08:51:39 AMI shall pay more attention when using params with invoke
The interesting point is that you don't have to pay attention. The assembler throws an error if you trash eax with an "addr". The same should happen with edx.
So the question here is "what's wrong?". This is a serious question, and we really need to see the source code where this happens. It could be a bug of the assembler. Therefore I'd really appreciate if you post a short example where invoke fails :thup:
Quote from: jj2007 on March 19, 2025, 08:56:57 AMQuote from: quocsan on March 19, 2025, 08:51:39 AMI shall pay more attention when using params with invoke
The interesting point is that you don't have to pay attention. The assembler throws an error if you trash eax with an "addr". The same should happen with edx.
So the question here is "what's wrong?". This is a serious question, and we really need to see the source code where this happens. It could be a bug of the assembler. Therefore I'd really appreciate if you post a short example where invoke fails :thup:
I shall get back the erroneous version.
Now I think the error was not as I thought.
Quote from: quocsan on March 19, 2025, 09:06:56 AMI shall get back the erroneous version.
Thanks, that's really interesting :thup:
Quote from: jj2007 on March 19, 2025, 08:56:57 AMQuote from: quocsan on March 19, 2025, 08:51:39 AMI shall pay more attention when using params with invoke
The interesting point is that you don't have to pay attention. The assembler throws an error if you trash eax with an "addr". The same should happen with edx.
How would EDX ever get involved with
INVOKE? No interaction there that I know of.
EDX is what I usually use instead of EAX whenever there's an
ADDR in the mix.
Hi all!
Below is the old version of my source code. You can find wsprint easily.
I hope you will show me the light.
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
WinMain proto CALLBACK :DWORD, :DWORD, :DWORD, :DWORD
SelectFile proto
DbfID proto :Byte
ProcessDBF proto
DbfMinFileSize EQU 328 ; 0x148, Estimated value.
.data
DbfHeader struct
m_ID DB ?
m_UpdatedYY DB ? ; YYMMDD
m_UpdatedMM DB ?
m_UpdatedDD DB ?
m_RecordCount DD ?
m_HeaderSize DW ?
m_RecordSize DW ?
m_Reserved DB 16 DUP(?)
m_MDXflag DB ?
m_CodePage DB ?
m_Reserved2 DB 2 DUP(?)
DbfHeader EndS
Dbf_IDx struct
Desc DD ?
ID DB ?
Dbf_IDx EndS
_TITLE DB "DBF checker", 0
_EOPENFILE DB "Error opening file.", 0
_ESEEKFILE DB "Error moving file pointer.", 0
_EREADFILE DB "Error reading file.", 0
_EWRITEFILE DB "Error updating file.", 0
_EFMT_DBF DB "Invalid header format for DBF file.", 0
_VALID_DBF DB "The file structure seems to be OK.", 0
_OFN_TITLE DB "Choose the DBF file to check/fix", 0
szFilter DB "DBF files (*.DBF)", 0, "*.DBF", 0, 0
szFsizeFmt DB "Error %lu: GetFileSize failed.", 0Ah, 0
szMsg DD OFFSET _VALID_DBF
szFmt DB "File name: [%s]", 0Ah
DB "File size: %lu (bytes)", 0Ah
; DB "ID: 0x%02X [%s]", 0Ah
DB "ID: 0x%02X", 0Ah
DB "Last updated: %02d/%02d/%04d", 0Ah
DB "Number of records: %d", 0Ah
DB "Header size: %d (bytes)", 0Ah
DB "Record size: %d (bytes).", 0Ah, 0Ah
DB "Successfully fixed the file structure.", 0
; DBF's IDs
_x02 DB "FoxBASE 1.0", 0
_x03 DB "FoxBASE 2.x/ Dbase III plus, no memo", 0
_x04 DB "dBASE IV, no memo", 0
_x05 DB "dBASE V, no memo", 0
_x07 DB "Visual Objects for the dBASE III files, no memo", 0
_x30 DB "Visual FoxPro", 0
_x31 DB "Visual FoxPro, with auto increment", 0
_x32 DB "Visual FoxPro with Varchar/ Varbinary", 0
_x43 DB "dBASE IV SQL table files, no memo", 0
_x7B DB "dBASE IV with memo", 0
_x63 DB "dBASE IV SQL system files, no memo", 0
_x83 DB "FoxBASE 2.x/ dBASE III PLUS, with memo", 0
_x87 DB "Visual Objects for the dBASE III files, with memo", 0
_x8B DB "dBASE IV with memo", 0
_x8E DB "dBASE IV with SQL table", 0
_xCB DB "dBASE IV SQL table files, with memo", 0
_xF5 DB "FoxPro 2.x with memo", 0
_xFB DB "FoxBASE/ FoxPro 2", 0
aID LABEL BYTE ; Array of IDs
Dbf_IDx <OFFSET _x02, 002h>
Dbf_IDx <OFFSET _x03, 003h>
Dbf_IDx <OFFSET _x04, 004h>
Dbf_IDx <OFFSET _x05, 005h>
Dbf_IDx <OFFSET _x07, 007h>
Dbf_IDx <OFFSET _x30, 030h>
Dbf_IDx <OFFSET _x31, 031h>
Dbf_IDx <OFFSET _x32, 032h>
Dbf_IDx <OFFSET _x43, 043h>
Dbf_IDx <OFFSET _x7B, 07Bh>
Dbf_IDx <OFFSET _x63, 063h>
Dbf_IDx <OFFSET _x83, 083h>
Dbf_IDx <OFFSET _x87, 087h>
Dbf_IDx <OFFSET _x8B, 08Bh>
Dbf_IDx <OFFSET _x8E, 08Eh>
Dbf_IDx <OFFSET _xCB, 0CBh>
Dbf_IDx <OFFSET _xF5, 0F5h>
Dbf_IDx <OFFSET _xFB, 0FBh>
aIdSize EQU ($ - OFFSET aID) / sizeof Dbf_IDx
.data?
ZeroMark LABEL BYTE
hInstance HINSTANCE ?
CommandLine LPSTR ?
fname DB MAX_PATH DUP(?)
szDir DB MAX_PATH DUP(?)
ofn OPENFILENAME <?>
szBuffer DB 512 DUP(?)
Header DbfHeader <?>
DbfDesc DD ?
hFile DD ?
lBytesIO DD ?
FileSize DD ?
fSuccess DD ?
RecCount DD ?
ZeroFillSize EQU $ - OFFSET ZeroMark
.code
start: ; Main entry point.
invoke RtlZeroMemory, addr ZeroMark, ZeroFillSize ; Zero-fill variable.
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke GetCommandLine
mov CommandLine, eax
invoke WinMain, hInstance, NULL, CommandLine, SW_SHOWDEFAULT
invoke ExitProcess, eax
WinMain Proc Inst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
invoke SelectFile
.IF eax == TRUE
invoke ProcessDBF
.ENDIF
ret
WinMain EndP
;
; Returns Desc if ID is valid, NULL otherwise.
DbfID proc uses edi ID:Byte
xor eax, eax
push eax
pop ecx ; ecx=eax=0
mov dl, ID
mov edi, OFFSET aID
sub edi, sizeof Dbf_IDx
assume edi: ptr Dbf_IDx
ID_check:
add edi, sizeof Dbf_IDx
cmp [edi].ID, dl
jne ID_next
mov eax, [edi].Desc
jmp ID_done
ID_next:
inc ecx
cmp ecx, aIdSize
jb ID_check
ID_done:
assume edi: nothing
ret
DbfID EndP
SelectFile Proc
invoke GetCurrentDirectory, MAX_PATH, addr szDir
push OFFSET szDir
pop ofn.lpstrInitialDir
mov ofn.lStructSize, sizeof OPENFILENAME
mov ofn.hwndOwner, NULL
push OFFSET szFilter
pop ofn.lpstrFilter
mov ofn.nFilterIndex, 1
push OFFSET fname
pop ofn.lpstrFile
push OFFSET _OFN_TITLE
pop ofn.lpstrTitle
mov ofn.nMaxFile, MAX_PATH
mov ofn.Flags, OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST or OFN_FORCESHOWHIDDEN or OFN_EXPLORER
invoke GetOpenFileName, addr ofn
ret
SelectFile EndP
ProcessDBF Proc
mov hFile, INVALID_HANDLE_VALUE
invoke SetFileAttributes, addr fname, FILE_ATTRIBUTE_NORMAL; Force file attr to be normal.
invoke CreateFile, addr fname, GENERIC_READ or GENERIC_WRITE, NULL, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
mov hFile, eax
.IF eax == INVALID_HANDLE_VALUE
mov szMsg, OFFSET _EOPENFILE
jmp Process_Done
.ENDIF
invoke GetFileSize, hFile, NULL
mov FileSize, eax
.IF eax == INVALID_FILE_SIZE
invoke GetLastError
invoke wsprintf, addr szBuffer, addr szFsizeFmt, eax
mov szMsg, OFFSET szBuffer
jmp Process_Done
.ENDIF
invoke ReadFile, hFile, addr Header, sizeof Header, addr lBytesIO, 0
mov fSuccess, eax
.if (!eax || lBytesIO != sizeof Header)
mov szMsg, OFFSET _EREADFILE
jmp Process_Done
.endif
invoke DbfID, Header.m_ID
mov DbfDesc, eax ; Save ID description
.if (FileSize < DbfMinFileSize || !eax || !Header.m_RecordSize || !Header.m_HeaderSize)
mov szMsg, OFFSET _EFMT_DBF
mov fSuccess, FALSE
jmp Process_Done
.endif
xor edx, edx
push edx
pop ecx
mov eax, FileSize
mov cx, Header.m_HeaderSize
sub eax, ecx ; (FileSize - Header.m_HeaderSize)
mov cx, Header.m_RecordSize
div ecx
mov RecCount, eax ; = (FileSize - Header.m_HeaderSize) / Header.m_RecordSize
.if (eax != Header.m_RecordCount) ; Different in record counts!
invoke SetFilePointer, hFile, 0, 0, FILE_BEGIN
.if (eax == INVALID_SET_FILE_POINTER)
mov szMsg, OFFSET _ESEEKFILE
mov fSuccess, FALSE
jmp Process_Done
.endif
push RecCount
pop Header.m_RecordCount ; Update correct number of records ...
; ... and write back to file at the appropriate file offset
mov szMsg, OFFSET _EWRITEFILE ; assume error writing
invoke WriteFile, hFile, addr Header, sizeof Header, addr lBytesIO, NULL
mov fSuccess, eax
.if (eax != 0 && lBytesIO == sizeof Header)
invoke FlushFileBuffers, hFile
xor edx, edx
mov dl, Header.m_UpdatedYY ; year of updated date
.if (dl < 80)
add dx, 2000
.else
add dx, 1900
.endif
invoke wsprintf, \
addr szBuffer, \
addr szFmt, \
addr fname, \
FileSize, \
Header.m_ID, \ ;DbfDesc, \
Header.m_UpdatedDD, Header.m_UpdatedMM, edx, \
[Header.m_RecordCount], \
[Header.m_HeaderSize], \
[Header.m_RecordSize]
mov szMsg, OFFSET szBuffer ; to show detailed information.
.endif
.endif
Process_Done:
.IF hFile != INVALID_HANDLE_VALUE
invoke CloseHandle, hFile
.ENDIF
.IF fSuccess != 0
mov eax, MB_ICONINFORMATION
.ELSE
mov eax, MB_ICONERROR
.ENDIF
invoke MessageBox, NULL, szMsg, addr _TITLE, eax
mov eax, fSuccess
ret
ProcessDBF EndP
end start
In your code you have
invoke wsprintf, addr szBuffer, addr szFsizeFmt, eax
invoke wsprintf, \
addr szBuffer, \
addr szFmt, \
addr fname, \
FileSize, \
Header.m_ID, \ ;DbfDesc, \
Header.m_UpdatedDD, Header.m_UpdatedMM, edx, \
[Header.m_RecordCount], \
[Header.m_HeaderSize], \
[Header.m_RecordSize]
Both of those look fine to me, although I haven't checked the call to wsprintf() against the format string. You seem to understand that the parameter list must match the sequence of format specifiers (%02X, %02d, etc.).
So what's the problem you want us to help you with?
One thing I'm not sure of is the format specifier %lu; is this supposed to be a DWORD or is a QWORD (8 bytes)? If it's just a DWORD then just use %u, which is a standard integer (32 bits).
And it's OK to use EAX as the parameter here, since it's to the right of the first ADDR in the parameter list. If there's a problem there the assembler will issue a warning that the register will be overwritten.
One thing I'd change is to use carriage-return/line-feed pairs at the ends of lines, instead of just the line feeds that you have:
13, 10
instead of just 0Ah (and no reason to use hex here)
Well, as you noted, I doubt that my problem is from the size of parameters.
I shall check this matter when I have time.
Thank you so much for your help, NoCforMe!
So what is your problem, exactly?
Quote from: NoCforMe on March 19, 2025, 12:11:07 PMSo what is your problem, exactly?
(https://i.postimg.cc/sB1Pf8gX/result.jpg) (https://postimg.cc/sB1Pf8gX)
As you can see in the attached image: The resulted string after calling
wspintf was not as expected. 6 numbers from after "
Last updated: " were all wrong.
At that moment, I was in a hurry, so I had to fix the problem by preparing the call to
wspintf by hand, instead of using
invoke. That's all.
I think I found your problem:
Format for printing the date:
DB "Last updated: %02d/%02d/%04d"
Source of the date data:
DbfHeader struct
m_ID DB ?
m_UpdatedYY DB ? ; YYMMDD
m_UpdatedMM DB ?
m_UpdatedDD DB ?
Do you see the problem? The first two fields you're trying to use are defined in the structure as bytes (Header.m_UpdatedDD and Header.m_UpdatedMM); however, wsprintf() is using signed DWORD fields (%02d) to format this data, so it's actually using DWORDs for each field instead of just the one byte.
You can't do this kind of conversion (from a byte to a DWORD) with wsprintf(). You need to extract these two fields, convert them to a DWORD and then pass them as parameters (either in registers or in local variables). That should fix your problem.
You can most easily get them into a DWORD thus:
MOVZX EAX, Header.m_UpdatedMM ;Load a byte and zero-extend it to a DWORD.
MOV localMM, EAX
then use localMM as the parameter for wsprintf().
(You should do the same thing with m_ID, since it's also a byte in the structure.
Or alternatively you could make the structure fields DWORDs and store them that way, in which case you can directly use them as parameters to pass to wsprintf().)
Good job, NoCforMe!
I know the cause now and will avoid the same fault in future.
Thank you so much for your help!
Happy to be of assistance.
Just remember to always keep in mind The Size of Things, since assembly language doesn't have all the guard rails that C or C++ does.
Which version of ML are you using? Some early versions had trouble passing bytes as arguments.
I built the code with the latest ML (14.42.34438.0) and it seemed to give proper values, except the year was 0001.
Microsoft (R) Macro Assembler Version 6.14.8444.
Now I'm editing my source code as NoCforMe guide.
Quote from: sinsi on March 19, 2025, 01:49:40 PMWhich version of ML are you using? Some early versions had trouble passing bytes as arguments.
I built the code with the latest ML (14.42.34438.0) and it seemed to give proper values, except the year was 0001.
So how does
wsprintf()* handle byte arguments? does it "promote" them automagically?
I thought not, but who knows?
Post some executable code? I'm curious.
* or any other function for that matter
The result is perfect!
.if (eax != 0 && lBytesIO == sizeof Header)
invoke FlushFileBuffers, hFile
movzx edx, Header.m_UpdatedYY ; year of updated date
.if (dl < 80)
add dx, 2000
.else
add dx, 1900
.endif
movzx ecx, Header.m_UpdatedMM
movzx ebx, Header.m_UpdatedDD
movzx eax, Header.m_ID
movzx edi, Header.m_HeaderSize
movzx esi, Header.m_RecordSize
invoke wsprintf, \
addr szBuffer, \
addr szFmt, \
addr fname, \
FileSize, \
eax, \
DbfDesc, \
ebx, ecx, edx, \
Header.m_RecordCount, \
edi, \
esi
mov szMsg, OFFSET szBuffer ; to show detailed information.
.endif
Comparison
Macro Assembler Version 6.14.8444 Macro Assembler Version 14.42.34438.0
push 0 push small 0
push small [word_40388A] push small [word_40388A]
push 0 push small 0
push small [word_403888] push small [word_403888]
push dword_403884 push dword_403884
push edx push edx
push 0 push small 0
mov al, byte_403882 mov al, byte_403882
movzx ax, al movzx ax, al
push ax push ax
push 0 push small 0
mov al, byte_403883 mov al, byte_403883
movzx ax, al movzx ax, al
push ax push ax
push 0 push small 0
mov al, byte_403880 mov al, byte_403880
movzx ax, al movzx ax, al
push ax push ax
push dword_4038AC push dword_4038AC
push offset FileName push offset FileName
push offset aFileNameSFileS push offset aFileNameSFileS
push offset stru_403634.pvReserved push offset stru_403634.pvReserved
call wsprintfA call wsprintfA
add esp, 2Ch add esp, 2Ch
Notice push 0 and push small 0?
ML6 pushes a 32-bit 0 then a word -> 48 bits
ML14 pushes a 16-bit 0 then a word -> 32 bits - correct
The problem occurs anywhere that you supply a byte as an argument in 32-bit code.
Another reason to ditch ML6.
Quote from: sinsi on March 19, 2025, 02:28:20 PMThe problem occurs anywhere that you supply a byte as an argument in 32-bit code.
Another reason to ditch ML6.
OK;
1. I guess it never occurred to me to even try that (use a byte argument in the first place with the hope that "someone" would "take care of it"). So no reason here not to use the old stuff.
2. Never saw the qualifier "small" before: means byte? word?
Quote from: quocsan on March 19, 2025, 02:25:35 PMThe result is perfect!
.if (eax != 0 && lBytesIO == sizeof Header)
invoke FlushFileBuffers, hFile
movzx edx, Header.m_UpdatedYY ; year of updated date
.if (dl < 80)
add dx, 2000
.else
add dx, 1900
.endif
movzx ecx, Header.m_UpdatedMM
movzx ebx, Header.m_UpdatedDD
movzx eax, Header.m_ID
movzx edi, Header.m_HeaderSize
movzx esi, Header.m_RecordSize
invoke wsprintf, \
addr szBuffer, \
addr szFmt, \
addr fname, \
FileSize, \
eax, \
DbfDesc, \
ebx, ecx, edx, \
Header.m_RecordCount, \
edi, \
esi
mov szMsg, OFFSET szBuffer ; to show detailed information.
.endif
Good!
Since you're using non-volatile (i.e., "sacred") registers there--EBX, ESI, EDI--be sure to save and restore them around this code so they don't get trashed. (Probably not necessary if you're doing this in your "WinMain" code, or any top-level code in a Win32 program, but should always be done elsewhere.)
Quote from: NoCforMe on March 19, 2025, 02:42:20 PMGood!
Since you're using non-volatile (i.e., "sacred") registers there--EBX, ESI, EDI--be sure to save and restore them around this code so they don't get trashed. (Probably not necessary if you're doing this in your "WinMain" code, or any top-level code in a Win32 program, but should always be done elsewhere.)
Yes, I did some changes in the source code, including preserving registers.
ProcessDBF Proc uses ebx edi esi
mov RecCount, eax ; = (FileSize - Header.m_HeaderSize) / Header.m_RecordSize
.if (eax != Header.m_RecordCount) ; Different in record counts!
stupid question, what this "fix" means ?
DBF-file can have also deleted records, so actual file size can be bigger ?
EDIT: 'Number of records in the database file' can also include deleted records.
Most DBF writers just add new record end of file and don't touch deleted records.
EDIT:
Xbase Data file (*.dbf) (https://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT)
QuoteEnd-of-file
dBASE II regards any End-of-File 1Ah value as the end of the file. dBASE III regard an End-of-File as an ordinary character, however it appends an extra End-of-File character at the physical end of the file.
If the file is packed the physical size of the file may be larger than the logical i.e. there may be garbage after the EOF mark.
Quote from: TimoVJL on March 19, 2025, 05:01:43 PM mov RecCount, eax ; = (FileSize - Header.m_HeaderSize) / Header.m_RecordSize
.if (eax != Header.m_RecordCount) ; Different in record counts!
stupid question, what this "fix" means ?
DBF-file can have also deleted records, so actual file size can be bigger ?
A DBF file is corrupted when something in its header is wrong. According to my study, deleted records are marked with asterisks ('*') at the beginning of record data, and other details in its header remain as usual.
The fix I've done is: Updating the header with correct number of records.
Compliments, quocsan - you are a new member but apparently not a newbie :thumbsup: