Understand
the Boot
yep,
it's a full trackloader.
commented source :
move.l
#$7FFF7FFF,d0 ; stop operating system
move.l d0,$DFF09A ; all interruptions off
move.w d0,$DFF096 ; all dma off
lea TrackLoader(pc),a0 ; copy main prog to $60000
lea $60000,a1 ; and put addr $60000 in exception
vector $10
move.l a1,$10.w ; if a Illegal instruction occurs,
jump to vector $10
move.w #(end-TrackLoader)/4-1,d0
_copy:
move.l (a0)+,(a1)+
dbf d0,_copy
illegal ; start in Supervisor mode |
the
program start in Trace mode (TVD : Trace Vector Detection).
we can't use breakpoint. but we can use for example
a "btst #6,$bfe001" before jump.(08 39 00
06 00 bf e0 01 66 f6)
you can skip the trace mode : jump directly to $60000.
several ways : change "move.l a1,$10.w"
with pea $60000, and "illegal" with "rts",
and nop "move #$2000,sr".
now we access to the hardware custom trackloader,
the only one able to read this disk.
TrackLoader:
lea $400.w,sp ; Supervisor Stack at 400
move.l Datas(pc),-(sp) ; put Load buffer in stack
to jump at the end.
move #$2000,sr
lea
Datas(pc),a0
lea DelaySoft(pc),a5
lea $BFD100,a4 ; CIAB-PRB in A4
lea $DFF024,a6 ; DSKLEN custom base in A6
move.l #$55555555,d6 ; odd bits mask MFM uncode
move.l (a0)+,d5 ; Load address
move.l (a0)+,starttrk-vars(a5) ; start track*tracksize
move.l (a0)+,d4 ; len track*tracksize
move.l #$18B8,d3 ; custom track size
; (standard $1600)
move.b #$7F,(a4) ; Motor on
jsr (a5) ; cpu delay
bclr #3,(a4) ; DSKSEL0 select DF0
jsr (a5) ; cpu delay
bsr DiskReady
|
here,
it's the loader init. we recognize the MFM masks $55555555,
DSKLEN and CIA register who manage the disk drivers,
and start DF0
take a look at Datas :
dc.l $40000
dc.l $f28d8
dc.l $4a28
it's seems to be the file information : certainly
load buffer, start track, length.
let's continue to the load loop
retry
clr.w try-vars(a5)
Load:
move.l starttrk-vars(a5),d7
bsr SeekTrack
move.w #$8210,$96-$24(a6) ; DMACON enable DMA
Disk
.loopload:
move.w #$7F00,$9E-$24(a6) ; erase ADKCON
move.w #$9500,$9E-$24(a6) ; good value in ADKCON
move.w #$4124,$7E-$24(a6) ; DSKSYNC custom sync
word (standard 4489)
move.w #2,$9C-$24(a6) ; INTREQ DSKBLK interrupt
bsr DiskReady
move.l #$70000,$20-$24(a6) ; DSKPTH mfm buffer
address
move.w #$4000,(a6) ; erase DSKLEN
move.w #$8000+$18C4,(a6) ; DSKLEN raw read len
(words) standard $8000+$1900
move.w #$8000+$18C4,(a6) ; 2 times to start DMA
transfert
move.l #$A0000,d7 ; loop wait
.wait:
btst #1,$1F-$24(a6) ; wait for disk drive to be
ready
bne.s .continue
subq.l #1,d7
bne.s .wait
bra.s .loopload |
the
syncro word and the track len are customs : $4124
and $18C4. the standard values are $4489 and $1900.
let's see first the SeekTrack routine:
SeekTrack:
movem.l d0-d3,-(sp)
move.l d7,d0 ; start track
move.l d3,d2 ; track size 18B8
divu d2,d0 ; F28D8/18B8 = 157 ($9d)
move.w d0,d1 ; num track 157
swap d0
move.w d0,rest-vars(a5) ; rest of the division
ext.l d0
sub.l d0,starttrk-vars(a5) ; f28d8-rest
cmpi.w #80,d1 ; >80 ?
bge.s _greater
st head-vars(a5) ; side 0
bra.s _less
_greater:
sf head-vars(a5)
subi.w #80,d1 ; track on side 1
_less:
move.b d1,d7
move.b head-vars(a5),d0
beq.s Side1
bset #2,(a4) ; DSKSIDE side 0
bra.s seekthetrack
Side1:
bclr #2,(a4) ; DSKSIDE side 1
seekthetrack:
jsr (a5) ; cpu delay
tst.b track-vars(a5) ; track >=0
bge.s seekForw
bsr.s Seek0
seekForw:
cmp.b track-vars(a5),d7 ; current track = seek
track
beq.s seekOk
blt.s seekBack
bsr.s seekForward
bra.s seekIt
seekBack:
bsr.s seekBackward
seekIt:
bsr.s DiskReady ; CIAA-PRA - DSKRDY
bra.s seekForw
seekOk:
movem.l (sp)+,d0-d3
rts
noTrack0:
bsr.s seekBackward
Seek0:
btst #4,$F01(a4) ; $BFE001 CIAA-PRA - DSKTRK0
bne.s noTrack0
sf track-vars(a5) ; clr
rts
seekForward:
bclr #1,(a4) ; DSKDIR goto cyl 79
addq.b #1,track-vars(a5) ; incr current track
bra.s MoveHeads ; move +/-1 track
seekBackward:
bset #1,(a4) ; DSKDIR goto cyl 0
subq.b #1,track-vars(a5) ; decr current track
MoveHeads:
bclr #0,(a4) ; DSKSTEP step low
bset #0,(a4) ; step high
|
the
beginning of routine show that the value in d7 (start
track) is divided by the len of a track to get the
track num. If not integer division, the rest is sector
in track *512.
after, he compare the track number with 80. If geater,
then sub 80 and set flag side to 1.
This loader seems to use tracks 1-80 on lower side
first and tracks 81-160 on upper side. and not lower-upper-lower-upper...like
standard dos.
let's see now the decode routine:
.continue:
bsr.s MFMUncodeCheck
beq.s .ok
addq.w #1,try-vars(a5) ; nb try
cmpi.w #8,try-vars(a5)
bls.s Load
moveq #0,d7
pea retry-vars(a5) ; retry
bra.w SeekTrack
.ok:
movea.l buffer-vars(a5),a0 ; mfmbuffer (70000)
addq.w #8,a0 ; skip header sync
movea.l d5,a1 ; load address (40000)
move.w rest-vars(a5),d0
move.l d3,d7 ; track size
sub.w d0,d7 ; - real size read
lsr.w #2,d7 ; /2
subq.w #1,d7 ; -1
add.w d0,d0 ; rest*2 (words->bytes)
adda.w d0,a0 ; skipped in mfmbuffer
.decode:
move.l (a0)+,d0 ; even long word
move.l (a0)+,d1 ; odd long word
and.l d6,d0 ; mask bits
and.l d6,d1
add.l d1,d1 ; rotate odd bits to left
or.l d1,d0 ; merge odd and even bits
move.l d0,(a1)+ ; store decoded long word
subq.l #4,d4 ; len -4 bytes
beq.s StopDrive ; finished
dbf d7,.decode
add.l
d3,starttrk-vars(a5) ; next track
move.l a1,d5 ; load address for next track
bra Load
MFMUncodeCheck:
movem.l d1-a6,-(sp)
movea.l buffer-vars(a5),a0 ; mfmbuffer = 70000
bsr.s MFMchecksumHeader
move.l d0,d1 ; checksum
swap d1
cmpi.w #$5041,d1 ; checksum='PA' = "Pierre
Adane"
bne.s .error ; not a PA track
cmp.w track-vars(a5),d0 ; check track number
is
bne.s .error ; what we expected ? no
subq.l #8,a0 ; yep. start of data
move.w #($18C4-2)/4-1,d7 ; decode 1583.L (18B8+12-2
bytes)
moveq #0,d3 ; checksum
.decode:
move.l (a0)+,d0 ; even long word
move.l (a0)+,d2 ; odd long word
and.l d6,d0 ; mask bits
and.l d6,d2
add.l d0,d3 ; rotate odd bits to the left
add.l d2,d3 ; add to checksum
dbf d7,.decode
bsr.s
MFMchecksumHeader
cmp.l d0,d3 ; good checksum
bne.s .error ; no
movem.l (sp)+,d1-a6
moveq #0,d7 ; ok
rts
.error
movem.l (sp)+,d1-a6
moveq #-1,d7 ; error
rts
MFMchecksumHeader:
move.l (a0)+,d0 ; even MFM long word : $5249
move.l (a0)+,d1 ; odd MFM long word
and.l d6,d0 ; MFM mask
and.l d6,d1
add.l d1,d1 ; rotate odd bits left
or.l d1,d0 ; merge odd and even bits
rts
|
some
others routs:
StopDrive:
bset #7,(a4) ; DSKMOTOR off
bset #3,(a4) ; DSKSEL0 deselect df0
jsr (a5) ; delay
bclr #3,(a4)
jsr (a5) ; delay
bset #3,(a4)
rts ; exit then start @ 40000 (load buffer)
DiskReady:
btst #5,$F01(a4) ; $BFE001 CIAA-PRA - bit DSKRDY
bne.s DiskReady
rts
vars
; start of variables
DelaySoft:
move.w d7,-(sp)
move.w #10000,d7 ; ~7 ms
_wait:
dbf d7,_wait
move.w (sp)+,d7
rts
buffer dc.l $70000 ; mfm track buffer
try dc.w 0
rest dc.w 0
starttrk dc.l 0
track dc.b -1 ; track counter
head dc.b 0 ; side
|
the
delay is software : to fix!
this trackloader is a raw sector mfm loader (sector
precision).
now, we know how it works. I think/hope the game use
the same.
Rip
the Files
hmm...let's
continue loading of the first file at 40000 (3 tracks
from track 157).
disassemble
at 40000:
it seems to be a decrypt/copy routine. crypted data
at $400e8 copied to 60000.
Then
jump at 60000. First "protection". I skip
the "decrypt" routine here : not really
necessary with AR.
continue and disassemble 60000 (when loading restart):
yet another crypted program. we can read "Anti
L bootblock loader by Pierre ADANE". decrypted
to $488. Second "protection".
disassemble $488:
hmm...strange, but we can recognize the same routine
than in boot. it's the trackloader. other things seems
crypted. Third "protection".
search for file info...we have chance : at $52e, the
delay sub prog, and at $540, the variables :
$70000 (mfm buffer)
...
$60000 load address
$EF768 start track
$2710 len
let's
continue loading 2 tracks from track 155...
the
credits screen appear and loading continue.
stop and disassemble at 60000:
after VBI init, at $6009E he jump to trackloader $604d0.
it's the same.
File infos at 606cc:
$23800 load address
$D0908 start track 135
$1EE60 len (~20 tracks)
at
600a2, he put load address in stack (jump at the end
at $23800)
at 600b0, we can see the string "Ice!" :
the file is ice-packed.
he depack the file. ending at 60104, and jump to depacked
program at $23800.
after
loading and depacking,...loading restart.
the Intro appear.
take
a look at $23800. It's seems to be the main program
(I hope).
at 2a132, after Interrupt settings, he jump to 2B72E
: it's the trackloader.
hmm...not really the same. continue disassembling.
file info is always in A0. track size $18B8...
hard to follow. at 2b2b4, the sync word is different
: $4488
restart
load at 2a142 and stop just at the end.
take a look at 60000: "Ice!"
so, at 2a14e, he depack the file. UnICE is at 29F6A.
search occurences of trackloader ($2B72E).
called 6 times : 2A148, 2A20A, 2A234, 2A26E, 2A28A
and 2A2B2.
take a look at each.
at 2a284/2A2B2:
seems to select infos in a files table with the file
number.
the table start at 29ee6. then jump to D300 with pointers
to trackloader, unIce and file info as parameters.
at D300 It's certainly the level loader.
when we'll patch the trackloader at $2B72E, no need
to patch the loader.
get the files list (11 files).
at
this moment, I think we can start ripping these files
by using the trackloader directly for example in loader
part
at D4BE, put a "btst #6,$bfe001 bne D4BE"
(08 39 00 06 00 bf e0 01 66 f6) and change the file
info pointer by address of the file you want to download
at D4D0. like that, you get the original files unpacked
at $50858.
you have the size of the file in the table infos.
Save them.
the
file 10 seems to be the intro, the file 8 loaded at
D300 is the level loader. the files 1 to 7 are certainly
the 7 levels.
after ripping, we can take a look at each file.
If file is packed with Ice, you can depack it with
XFD. or with the builtin UnIce.
The level 1 is packed but levels are only datas.
to
rip the first 2 files (The Credits and the Main program)
use the credits loader... Both are packed.
It's
also possible to rip the trackloader (from the Credits
part) and make your own file grabber.
First
Try
making
of a first version :
copy the files ripped to a new blank disk. Depending
on the trackloader, copy sector by sector or track
by track. (we have chance this time, there is enought
space on the disk). You can use tools like SectorTrasher/Silents
or with AR.
I used MegaMon
for the all parts.
For
the trackloader, I chose the option to replace it
with a new trackloader able to read by sector.
As
the first files are packed, you either depack and
repack with another cruncher or Ice, or rip original
routine in the credits (unice is from 600a8 to 602b6).
To
keep things simple, and use the original depacker,
we will patch code after the depacking and before
executing (as WHDload Slaves).
It's
time to write our starter code and a boot.
the
starter program will load and unpack the credits part
and main program, replace trackloader, do the patchs
and start.
the whole things is more than 1kb, so we need to load
it from boot.
copy it just after boot, at sector 2. (Nothing here)
The
Boot: only use trackdisk device for loading
our little proggy.
dc.b
'DOS',0
dc.l 0
dc.l $370
move.w
#2,$1c(a1)
move.l #$20000,$28(a1)
move.l #2*512,$2c(a1) ; offset
move.l #4*512,$24(a1) ;len
movea.l 4.w,a6
jsr -$1c8(a6)
jmp $20000
|
compil
obj and copy as boot.
example with MegaMon and boot.exe :
L boot.exe 70000 ; load exe
b 70020 ; set new checksum (skip hunks)
>b 70020 0 2 ; copy new boot to the disk
The starter Patcher:
first we load credits:
;
load packed credits
move.w #2,$1c(a1)
move.l #$60000,$28(a1)
move.l #1733*512,$2c(a1) ; offset
move.l #20*512,$24(a1)
movea.l 4.w,a6
jsr -$1c8(a6)
move.w
#$7fff,$dff09a
move.w #$7fff,$dff09c
movea.l
#$60000,a0
moveq #0,d0
bsr Decrunch ; the unice routine
;
patch credits - trackload
lea $6009e,a0 ; skip trackloader
move.w #$4ef9,(a0)+ ; jmp
pea loadmain(pc)
move.l (sp)+,(a0)+
; start credits -> download main prog at
23800 / return
jmp $60000
continue:
|
sub
rout "loadmain":
loadmain:
lea FileMain(pc),a0
bsr.s Load ; our trackloader
bra continue
FileMain
dc.l $23800
dc.l 1485*512 ; main program
dc.l $1EE60 ;keep original size or 248*512 |
the
main program is loaded.
continue:
movea.l #$23800,a0
moveq #0,d0
bsr Decrunch
; patch main
; patch trackloader
lea $2b72e,a0
lea Load(pc),a1
move.w #$4ef9,(a0)+
move.l a1,(a0)+ |
now,
patch the files infos with their new positions:
;
patch files infos
lea $29ee6+4,a0
lea Files(pc),a1
moveq #11-1,d7 ; 11 files
.loop
move.l (a1)+,(a0)+ ; offset
lea 8(a0),a0 ; skip addr and len if original len
used
dbf d7,.loop
...
Files
; make your own
dc.l 11*512
dc.l 11*512+sector size
...
|
the
last thing, copy the trackloader in memory and start:
;
do a copy of trackloader
lea Load(pc),a0
lea $23800-sizetk,a1
move.l #sizetk-1,d7
cp
move.b (a0)+,(a1)+
dbf d7,cp
jmp
$23800 ; start
...
insert "loadmain"
Load:
include "trackloader.asm"
sizetk=*-Load
;
ICE Depacker
; a0=source
Decrunch:
include "tokiunice.asm"
sizet=*-starter
|
(I
found the address where I copy trackloader after some
tests)
copy the boot and starter, and boot your new release
with MegaMon :
l patch.exe ; load at addr. xxxxx or L patch.exe 70000
(and skip hunk - code must be relocatable)
>b xxxxxx 2 4 ; copy in the empty room of the first
track, after the boot.
all work fine
we can play ! let you die. there is the "continue"...lets
finish...he load scores.
and reload intro.
restart. play. if you die or if you finished level,
the game should load score or level 2. but...Crash!
?!? break with AR.
dump memory at 23000 : our trackloader is no more
here!
impossible
to replace original trackloader because he is puzzled
into the code.
we need to stock the trackloader at a safe place...
fill the space from $300 (sp) to $23800 (start of
main program) with a "mark" and test game
levels, scores...
take a look at $23000 to see where your mark is erased
: from 400 to 236a0
hmm...it's too short for a trackloader, even a tiny
one.
we need to make a copy somewhere and test if erased.
address $100 is perhaps safer. try.
at $100, it's erase from $24e.
the download buffer for files start at 50858 for levels
and 60000 for intro/scores.
the intro file loaded at 60000 (128kb) go until 7FD54
! level 1 loaded at 50858 go to 7E738.
we have a safe space between 7FD54 and 7FFFE (7FFFF
is used) = 682 bytes maximum.
The game use only 512 Kb Chip but almost the whole
memory space!
phew,
what a problem!
we need a tiny trackloader.
very difficult if we want to keep the 512K max memory.
else we can test if extra mem (fast/chip) exists for
copying the trackloader.
the
smallest trackloader is from Alpha One (404 bytes)
: it's only a Trackloader, but it's possible to adapt
it for sector reading. (we have 280 bytes left).
or modify original one with standard values. or write
your own ;)
change
the starter code:
continue:
movea.l #$23800,a0
moveq #0,d0
bsr Decrunch
; patch main
; patch trackloader
lea $2b72e,a0
move.w #$4ef9,(a0)+
move.l #$7fd54,(a0) ; we have more space here.
lea
load(pc),a0
lea $7fd54,a1
move.l #sizepl-1,d7
.cppl
move.b (a0)+,(a1)+
dbf d7,.cppl
|
compile,
copy the new starter and restart. It seems better.
no more crash.
TESTING:
we
have the numlevel at 239C8, we can test each level.
you can also access to the outro : at the start, he
checks for level number 8 but it's where he put 1
in levelnum: change it.
at
level2 ... screen stay black and "flash"
before crash...another bug? a protection ?
levels 3,4,5,6 and 7 works.
CRACKING
so,
now we have to find if there are any protections:
restart at level 2 and let's crash and stop. why crash
?
see registers
PC=??? it's not a good address for a jump!
dump stack at 2f8 (register A7):
next line to execute : 3C9EE.
it's from the sub rout at 3C9D8
this rout use registers d0 and d2 to get jump address
in A0
err...d0=239c8 (level number)=$A57B x 4=(2)95ec.w
! it's not a good level number
and d2=1917d4 -> A0=xxxxxxxxxx ! bad address
where
is modified the levelnum ? certainly not with its
real address.
we
have to understand what happen before crash : restart
level 2.
stop and disassemble before crash. we are at 3c396.
cross
backward until you reach the start of the routine
: 3c26a
called from 2a40c <- from 2a342 (sub prog 2a2ee)
<- from 2a2ce (2a226)
first sub called is 3ca66.
using num level to jump to a routine.
d 3d7c2 (level 1)
d 3da1e (level 2)
check
differences between 2 routs
3DA1E:
we
can see a strange address in A1 : 1EB53 ? not in the
range of program.
d1 take its value from a line of code: rts = $4e75
he put d0 in 1EB53+d1=239C8...the level number address
!
at 3db06 : a loop where "d0" is computed
with values from table 24482-258e6.
D0
need to be Null : NOP the line 3DB06.
and skip the test of D0. change BEQ at 3DBB8 with
BRA (=$60) (both is better than only one)
Test
that
works !
add
the crack of the protection in Starter:
...patch
main...
; crack
; skip checksum
lea $3db06,a0
move.w #$4e71,(a0) ; nop
lea $3dbb8,a0
move.b #$60,(a0) ; bra |
and
test your new release.
If there is a checksum somewhere, why not more ?
test
the game.
If you play the first level quickly and continue,
no problems.
But if you loose and continue, or wait until the timer
reach ~1:00, the Toki Gfx become corrupted.
when
you continue, the whole screen is corrupted.
argl!
another protection!
I
tested level 2 and 3, waiting the gfx bug, sometime
more than 6 minutes. nothing seems happen.
The
bug appear during the game, not at the start like
the level 2.
not simple. hmm...I'm searching for the use of numlevel
to jump to the level manager (like level 2).
called always like that:
move.w $239c8,d0 ; 3039000239C8
add.w d0,d0 ; D040
add.w d0,d0
search the bytes: 30 39 00 02 39 C8 D0 40
the
last is already known (3CA66 init level with first
checksum).
check others:
1 and 4 are not used for a jump.
2,3 and 5 yes.
- nothing at 5 : RTS for level 1
only 2 left!
test: put a RTS at the second (2BC64) and try
oops! we got invulnerability against shot for enemies!
not good at all. but gfx bugs are still there.
redo
same thing for the third (2EB56). eh! no more enemies
nor others moving objects... wait end of timer...no
gfx bug !
disassemble the rout at 2EB72 (compare with level
2 for example):
search something odd...
a
test and below a modified address.
what is these variables ? already called somewhere?
23A82 (counter) and 23C56 are used at 2C8B2.
the long word at 23AAE already set only and table
3BEB4 are used here. 2C8B2 is called from 2AFBC and
2C842. all these routs seems to be a checksum.
and :
an
hidden call to 2C8B2 !
skip
the rout : replace bne after tst or beq after sub
by bra at 2ef1a or 2ef28)
the game seems ok. and no gfx bug
you
can add the patch:
;
checksum 2 (in level 1 routine)
lea $2ef1a,a0 ; or $2ef28
move.b #$60,(a0) ; bra |
pheww.
That's work now.
seems ok... difficult to see this kind of protections.
...enjoy...play... ... plop... a bug ?... after ~8
minutes :
hum...
It seems to be a third checkum !
from here, we can see the bitplan address of the game
panel at $12AA1518 ! (AR c1 -> $23858 : $E012AA,$E21518)
another gfx pointer modified. Note that crash could
start wherever in the game after 8 minutes : So, the
counter and the checksum
could not be in the main program, but in VBI. The
VBI is set at $35BCA. There are 2 routines (JSR).
To find the routine where the
checksum is, disable one of theses routines. We can
see that is the first one : the music player called
at $2BFEC.
If you search the value $12AA1518, you found the address
of the modified pointer (screen pointer) : $26594
and when you disassemble the play music, we can see
this address used (relative access -5D48(a2) ).
You can also compare the player code from the score
or the end part and see the checksum code just before
the vbl delay/dma setting code.
at
2C25C, test a counter : if not 0, then decrement counter
and compute checksum.
if you search the address 26594 (relative address!),
you can see it's a bitplan pointer for the game screen!
if counter=0, change bitplan address with checksum
value.
desactive it : put
a BRA $2C274 to 2C25C or 2C25E ($6016 or $6014)
update
our starter proggy:
;
skip check checksum 3 (modify a screen pointer
in puma replayer!)
lea $2c25e,a0
move.w #$6014,(a0) ; bra ok1 |
Play
: Seems ok now !
This protection is also used in Liquid Kids
and Snow Bros. Take a look at the sources of
WHDLoad
patch by CFOU! The Checksum+Counter routine is
very well explained.
TRAINING
Use
the original Cheatcode to Test the game : During
play, type KILLER (the border should
flash). This will give you infinite credits.
Now use the following keys:
F1-F7 : Skip to corresponding level.
F8 : Ending.
bonus : R : Flips the screen upside down.
Or
make you own trainer.
at start of main program, near 2a17e, he set some
variables :
we already know the level number at 239c8 (select/skip
level).
lives counter (start with 6) at 23CD9 (.b) : try to
change it.
search for live counter decrement.
at 2C9F0: 6x nop or replace instruction. a TST for
example ($4A)
we
can see in each routine, the variable 27E8C is initialized
with a longword like "40A0301" for level
1, "50A0301" for level 2, "60A0301"
for level 3...
first level timer start at 4:30, level 2 at 6:30,
level 3 at 5:30 !!
we have found the timer (unlimited time reached ;)
see trainer)
try to change the first byte : it's the minutes value.
search this address: f 02 7e 8c,23800 50858
check
each address.
at the first one, he test when minute=0, and decrement
at 370CC :
replace the subq.b by TST ($4A).
at the second one, he write the timer on info panel
(we can see 4E230 address).
at the third and fourth, he test the timer when all
parts are null.
others addresses are the initialization of the timer.
seems
to be ok. I hope (not yet fully tested ;) )
Enjoy
2008
(c) Heavy/VrS!/Cyb