cc68a136 |
1 | \r |
2 | _____ __ \r |
3 | / ___/__ __ ____ / /___ ___ ___ ___________________ \r |
4 | / /__ / // // __// // _ \ / _ \/ -_) ___________________ \r |
5 | \___/ \_, / \__//_/ \___//_//_/\__/ ___________________ \r |
6 | /___/ \r |
7 | ___________________ ____ ___ ___ ___ ___ \r |
8 | ___________________ / __// _ \ / _ \ / _ \ / _ \ \r |
9 | ___________________ / _ \/ _ // // // // // // / \r |
10 | \___/\___/ \___/ \___/ \___/ \r |
11 | \r |
12 | ___________________________________________________________________________\r |
13 | \r |
14 | Cyclone 68000 (c) Copyright 2004 Dave. Free for non-commercial use\r |
15 | \r |
16 | Homepage: http://www.finalburn.com/\r |
03c5768c |
17 | Dave's e-mail: emudave(atsymbol)googlemail.com\r |
cc68a136 |
18 | Replace (atsymbol) with @\r |
19 | \r |
03c5768c |
20 | Additional coding and bugfixes done by notaz, 2005-2007\r |
21 | Homepage: http://mif.vu.lt/~grig2790/Cyclone/ , http://notaz.gp2x.de\r |
cc68a136 |
22 | e-mail: notasas(atsymbol)gmail.com\r |
23 | ___________________________________________________________________________\r |
24 | \r |
25 | \r |
26 | What is it?\r |
27 | -----------\r |
28 | \r |
29 | Cyclone 68000 is an emulator for the 68000 microprocessor, written in ARM 32-bit assembly.\r |
30 | It is aimed at chips such as ARM7 and ARM9 cores, StrongARM and XScale, to interpret 68000\r |
31 | code as fast as possible.\r |
32 | \r |
33 | Flags are mapped onto ARM flags whenever possible, which speeds up the processing of opcode.\r |
34 | \r |
35 | \r |
36 | What's New\r |
37 | ----------\r |
03c5768c |
38 | v0.0087 notaz\r |
39 | - Reduced amount of code in opcode handlers by ~23% by doing the following:\r |
40 | - Removed duplicate opcode handlers\r |
41 | - Optimized code to use less ARM instructions\r |
42 | - Merged some duplicate handler endings\r |
43 | + Cyclone now does better job avoiding pipeline interlocks.\r |
44 | + Replaced incorrect handler of DBT with proper one.\r |
1c88b865 |
45 | + Changed "MOVEA (An)+ An" behaviour.\r |
46 | + Fixed flag behaviour of ROXR, ASL, LSR and NBCD in certain situations.\r |
47 | Hopefully got them right now.\r |
03c5768c |
48 | + Additional functionality added for MAME and other ports (see config.h).\r |
49 | \r |
cc68a136 |
50 | v0.0086 notaz\r |
51 | + Cyclone now can be customized to better suit your project, see config.h .\r |
52 | + Added an option to compress the jumptable at compile-time. Must call CycloneInit()\r |
53 | at runtime to decompress it if enabled (see config.h).\r |
54 | + Added missing CHK opcode handler (used by SeaQuest DSV).\r |
55 | + Added missing TAS opcode handler (Gargoyles,Bubba N Stix,...). As in real genesis,\r |
56 | memory write-back phase is ignored (but can be enabled in config.h if needed).\r |
57 | + Added missing NBCD and TRAPV opcode handlers.\r |
58 | + Added missing addressing mode for CMP/EOR.\r |
59 | + Added some minor optimizations.\r |
60 | - Removed 216 handlers for 2927 opcodes which were generated for invalid addressing modes.\r |
61 | + Fixed flags for ASL, NEG, NEGX, DIVU, ADDX, SUBX, ROXR.\r |
62 | + Bugs fixed in MOVEP, LINK, ADDQ, DIVS handlers.\r |
63 | * Undocumented flags for CHK, ABCD, SBCD and NBCD are now emulated the same way as in Musashi.\r |
64 | + Added Uninitialized Interrupt emulation.\r |
65 | + Altered timing for about half of opcodes to match Musashi's.\r |
66 | \r |
67 | v0.0082 Reesy\r |
68 | + Change cyclone to clear cycles before returning when halted\r |
69 | + Added Irq call back function. This allows emulators to be notified\r |
70 | when cyclone has taken an interrupt allowing them to set internal flags\r |
71 | which can help fix timing problems.\r |
72 | \r |
73 | v0.0081 notaz\r |
74 | + .asm version was broken and did not compile with armasm. Fixed.\r |
75 | + Finished implementing Stop opcode. Now it really stops the processor.\r |
76 | \r |
77 | v0.0080 notaz\r |
78 | + Added real cmpm opcode, it was using eor handler before this.\r |
79 | Fixes Dune and Sensible Soccer.\r |
80 | \r |
81 | v0.0078 notaz\r |
82 | note: these bugs were actually found Reesy, I reimplemented these by\r |
83 | using his changelog as a guide.\r |
84 | + Fixed a problem with divu which was using long divisor instead of word.\r |
85 | Fixes gear switching in Top Gear 2.\r |
86 | + Fixed btst opcode, The bit to test should shifted a max of 31 or 7\r |
87 | depending on if a register or memory location is being tested.\r |
88 | + Fixed abcd,sbcd. They did bad decimal correction on invalid BCD numbers\r |
89 | Score counters in Streets of Rage level end work now.\r |
90 | + Changed flag handling of abcd,sbcd,addx,subx,asl,lsl,...\r |
91 | Some ops did not have flag handling at all.\r |
92 | Some ops must not change Z flag when result is zero, but they did.\r |
93 | Shift ops must not change X if shift count is zero, but they did.\r |
94 | There are probably still some flag problems left.\r |
95 | + Patially implemented Stop and Reset opcodes - Fixes Thunderforce IV\r |
96 | \r |
97 | v0.0075 notaz\r |
98 | + Added missing displacement addressing mode for movem (Fantastic Dizzy)\r |
99 | + Added OSP <-> A7 swapping code in opcodes, which change privilege mode\r |
100 | + Implemented privilege violation, line emulator and divide by zero exceptions\r |
101 | + Added negx opcode (Shining Force works!)\r |
102 | + Added overflow detection for divs/divu\r |
103 | \r |
104 | v0.0072 notaz\r |
105 | note: I could only get v0.0069 cyclone, so I had to implement these myself using Dave's\r |
106 | changelog as a guide.\r |
107 | + Fixed a problem with divs - remainder should be negative when divident is negative\r |
108 | + Added movep opcode (Sonic 3 works)\r |
109 | + Fixed a problem with DBcc incorrectly decrementing if the condition is true (Shadow of the Beast)\r |
110 | \r |
111 | v0.0069\r |
112 | + Added SBCD and the flags for ABCD/SBCD. Score and time now works in games such as\r |
113 | Rolling Thunder 2, Ghouls 'N Ghosts\r |
114 | + Fixed a problem with addx and subx with 8-bit and 16-bit values.\r |
115 | Ghouls 'N' Ghosts now works!\r |
116 | \r |
117 | v0.0068\r |
118 | + Added ABCD opcode (Streets of Rage works now!)\r |
119 | \r |
120 | v0.0067\r |
121 | + Added dbCC (After Burner)\r |
122 | + Added asr EA (Sonic 1 Boss/Labyrinth Zone)\r |
123 | + Added andi/ori/eori ccr (Altered Beast)\r |
124 | + Added trap (After Burner)\r |
125 | + Added special case for move.b (a7)+ and -(a7), stepping by 2\r |
126 | After Burner is playable! Eternal Champions shows more\r |
127 | + Fixed lsr.b/w zero flag (Ghostbusters)\r |
128 | Rolling Thunder 2 now works!\r |
129 | + Fixed N flag for .b and .w arithmetic. Golden Axe works!\r |
130 | \r |
131 | v0.0066\r |
132 | + Fixed a stupid typo for exg (orr r10,r10, not orr r10,r8), which caused alignment\r |
133 | crashes on Strider\r |
134 | \r |
135 | v0.0065\r |
136 | + Fixed a problem with immediate values - they weren't being shifted up correctly for some\r |
137 | opcodes. Spiderman works, After Burner shows a bit of graphics.\r |
138 | + Fixed a problem with EA:"110nnn" extension word. 32-bit offsets were being decoded as 8-bit\r |
139 | offsets by mistake. Castlevania Bloodlines seems fine now.\r |
140 | + Added exg opcode\r |
141 | + Fixed asr opcode (Sonic jumping left is fixed)\r |
142 | + Fixed a problem with the carry bit in rol.b (Marble Madness)\r |
143 | \r |
144 | v0.0064\r |
145 | + Added rtr\r |
146 | + Fixed addq/subq.l (all An opcodes are 32-bit) (Road Rash)\r |
147 | + Fixed various little timings\r |
148 | \r |
149 | v0.0063\r |
150 | + Added link/unlk opcodes\r |
151 | + Fixed various little timings\r |
152 | + Fixed a problem with dbCC opcode being emitted at set opcodes\r |
153 | + Improved long register access, the EA fetch now does ldr r0,[r7,r0,lsl #2] whenever\r |
154 | possible, saving 1 or 2 cycles on many opcodes, which should give a nice speed up.\r |
155 | + May have fixed N flag on ext opcode?\r |
156 | + Added dasm for link opcode.\r |
157 | \r |
158 | v0.0062\r |
159 | * I was a bit too keen with the Arithmetic opcodes! Some of them should have been abcd,\r |
160 | exg and addx. Removed the incorrect opcodes, pending re-adding them as abcd, exg and addx.\r |
161 | + Changed unknown opcodes to act as nops.\r |
162 | Not very technical, but fun - a few more games show more graphics ;)\r |
163 | \r |
164 | v0.0060\r |
165 | + Fixed divu (EA intro)\r |
166 | + Added sf (set false) opcode - SOR2\r |
167 | * Todo: pea/link/unlk opcodes\r |
168 | \r |
169 | v0.0059: Added remainder to divide opcodes.\r |
170 | \r |
171 | \r |
172 | The new stuff\r |
173 | -------------\r |
174 | \r |
175 | Before using Cyclone, be sure to customize config.h to better suit your project. All options\r |
176 | are documented inside that file.\r |
177 | \r |
178 | IrqCallback has been changed a bit, unlike in previous version, it should not return anything.\r |
179 | If you need to change IRQ level, you can safely do that in your handler.\r |
180 | \r |
181 | Cyclone has changed quite a bit from the time when Dave stopped updating it, but the rest of\r |
182 | documentation still applies, so read it if you haven't done that yet. If you have, check the\r |
183 | "Accessing ..." parts.\r |
184 | \r |
185 | \r |
186 | ARM Register Usage\r |
187 | ------------------\r |
188 | \r |
189 | See source code for up to date of register usage, however a summary is here:\r |
190 | \r |
191 | r0-3: Temporary registers\r |
192 | r4 : Current PC + Memory Base (i.e. pointer to next opcode)\r |
193 | r5 : Cycles remaining\r |
194 | r6 : Pointer to Opcode Jump table\r |
195 | r7 : Pointer to Cpu Context\r |
196 | r8 : Current Opcode\r |
197 | r9 : Flags (NZCV) in highest four bits\r |
198 | (r10 : Temporary source value or Memory Base)\r |
199 | (r11 : Temporary register)\r |
200 | \r |
201 | \r |
202 | How to Compile\r |
203 | --------------\r |
204 | \r |
205 | Like Starscream and A68K, Cyclone uses a 'Core Creator' program which calculates and outputs\r |
206 | all possible 68000 Opcodes and a jump table into files called Cyclone.s and .asm\r |
207 | It then assembles these files into Cyclone.o and .obj\r |
208 | \r |
209 | Cyclone.o is the GCC assembled version and Cyclone.obj is the Microsoft assembled version.\r |
210 | \r |
211 | First unzip "Cyclone.zip" into a "Cyclone" directory.\r |
212 | If you are compiling for Windows CE, find ARMASM.EXE (the Microsoft ARM assembler) and\r |
213 | put it in the directory as well or put it on your path.\r |
214 | \r |
215 | Open up Cyclone.dsw in Visual Studio 6.0, compile and run the project.\r |
216 | Cyclone.obj and Cyclone.o will be created.\r |
217 | \r |
218 | \r |
219 | Compiling without Visual C++\r |
220 | ----------------------------\r |
221 | If you aren't using Visual C++, it still shouldn't be too hard to compile, just get a C compiler,\r |
222 | compile all the CPPs and C file, link them into an EXE, and run the exe.\r |
223 | \r |
224 | e.g. gcc Main.cpp OpAny.cpp OpArith.cpp OpBranch.cpp OpLogic.cpp OpMove.cpp Disa.c\r |
225 | Main.exe\r |
226 | \r |
227 | \r |
228 | Adding to your project\r |
229 | ----------------------\r |
230 | \r |
231 | To add Cyclone to you project, add Cyclone.o or obj, and include Cyclone.h\r |
232 | There is one structure: 'struct Cyclone', and one function: CycloneRun\r |
233 | \r |
234 | Don't worry if this seem very minimal - its all you need to run as many 68000s as you want.\r |
235 | It works with both C and C++.\r |
236 | \r |
237 | Byteswapped Memory\r |
238 | ------------------\r |
239 | \r |
240 | If you have used Starscream, A68K or Turbo68K or similar emulators you'll be familiar with this!\r |
241 | \r |
242 | Any memory which the 68000 can access directly must be have every two bytes swapped around.\r |
243 | This is to speed up 16-bit memory accesses, because the 68000 has Big-Endian memory\r |
244 | and ARM has Little-Endian memory.\r |
245 | \r |
246 | Now you may think you only technically have to byteswap ROM, not RAM, because\r |
247 | 16-bit RAM reads go through a memory handler and you could just return (mem[a]<<8) | mem[a+1].\r |
248 | \r |
249 | This would work, but remember some systems can execute code from RAM as well as ROM, and\r |
250 | that would fail.\r |
251 | So it's best to use byteswapped ROM and RAM if the 68000 can access it directly.\r |
252 | It's also faster for the memory handlers, because you can do this:\r |
253 | \r |
254 | return *(unsigned short *)(mem+a)\r |
255 | \r |
256 | \r |
257 | Declaring Memory handlers\r |
258 | -------------------------\r |
259 | \r |
260 | Before you can reset or execute 68000 opcodes you must first set up a set of memory handlers.\r |
261 | There are 7 functions you have to set up per CPU, like this:\r |
262 | \r |
263 | static unsigned int MyCheckPc(unsigned int pc)\r |
264 | static unsigned char MyRead8 (unsigned int a)\r |
265 | static unsigned short MyRead16 (unsigned int a)\r |
266 | static unsigned int MyRead32 (unsigned int a)\r |
267 | static void MyWrite8 (unsigned int a,unsigned char d)\r |
268 | static void MyWrite16(unsigned int a,unsigned short d)\r |
269 | static void MyWrite32(unsigned int a,unsigned int d)\r |
270 | \r |
271 | You can think of these functions representing the 68000's memory bus.\r |
272 | The Read and Write functions are called whenever the 68000 reads or writes memory.\r |
273 | For example you might set MyRead8 like this:\r |
274 | \r |
275 | unsigned char MyRead8(unsigned int a)\r |
276 | {\r |
277 | a&=0xffffff; // Clip address to 24-bits\r |
278 | \r |
279 | if (a<RomLength) return RomData[a^1]; // ^1 because the memory is byteswapped\r |
280 | if (a>=0xe00000) return RamData[(a^1)&0xffff];\r |
281 | return 0xff; // Out of range memory access\r |
282 | }\r |
283 | \r |
284 | The other 5 read/write functions are similar. I'll describe the CheckPc function later on.\r |
285 | \r |
286 | Declaring a CPU Context\r |
287 | -----------------------\r |
288 | \r |
289 | To declare a CPU simple declare a struct Cyclone in your code. For example to declare\r |
290 | two 68000s:\r |
291 | \r |
292 | struct Cyclone MyCpu;\r |
293 | struct Cyclone MyCpu2;\r |
294 | \r |
295 | It's probably a good idea to initialise the memory to zero:\r |
296 | \r |
297 | memset(&MyCpu, 0,sizeof(MyCpu));\r |
298 | memset(&MyCpu2,0,sizeof(MyCpu2));\r |
299 | \r |
300 | Next point to your memory handlers:\r |
301 | \r |
302 | MyCpu.checkpc=MyCheckPc;\r |
303 | MyCpu.read8 =MyRead8;\r |
304 | MyCpu.read16 =MyRead16;\r |
305 | MyCpu.read32 =MyRead32;\r |
306 | MyCpu.write8 =MyWrite8;\r |
307 | MyCpu.write16=MyWrite16;\r |
308 | MyCpu.write32=MyWrite32;\r |
309 | \r |
310 | You also need to point the fetch handlers - for most systems out there you can just\r |
311 | point them at the read handlers:\r |
312 | MyCpu.fetch8 =MyRead8;\r |
313 | MyCpu.fetch16 =MyRead16;\r |
314 | MyCpu.fetch32 =MyRead32;\r |
315 | \r |
316 | ( Why a different set of function pointers for fetch?\r |
317 | Well there are some systems, the main one being CPS2, which return different data\r |
318 | depending on whether the 'fetch' line on the 68000 bus is high or low.\r |
319 | If this is the case, you can set up different functions for fetch reads.\r |
320 | Generally though you don't need to. )\r |
321 | \r |
322 | Now you are nearly ready to reset the 68000, except you need one more function: checkpc().\r |
323 | \r |
324 | The checkpc() function\r |
325 | ----------------------\r |
326 | \r |
327 | When Cyclone reads opcodes, it doesn't use a memory handler every time, this would be\r |
328 | far too slow, instead it uses a direct pointer to ARM memory.\r |
329 | For example if your Rom image was at 0x3000000 and the program counter was $206,\r |
330 | Cyclone's program counter would be 0x3000206.\r |
331 | \r |
332 | The difference between an ARM address and a 68000 address is also stored in a variable called\r |
333 | 'membase'. In the above example it's 0x3000000. To retrieve the real PC, Cyclone just\r |
334 | subtracts 'membase'.\r |
335 | \r |
336 | When a long jump happens, Cyclone calls checkpc(). If the PC is in a different bank,\r |
337 | for example Ram instead of Rom, change 'membase', recalculate the new PC and return it:\r |
338 | \r |
339 | static int MyCheckPc(unsigned int pc)\r |
340 | {\r |
341 | pc-=MyCpu.membase; // Get the real program counter\r |
342 | \r |
343 | if (pc<RomLength) MyCpu.membase=(int)RomMem; // Jump to Rom\r |
344 | if (pc>=0xff0000) MyCpu.membase=(int)RamMem-0xff0000; // Jump to Ram\r |
345 | \r |
346 | return MyCpu.membase+pc; // New program counter\r |
347 | }\r |
348 | \r |
349 | Notice that the membase is always ARM address minus 68000 address.\r |
350 | \r |
351 | The above example doesn't consider mirrored ram, but for an example of what to do see\r |
352 | PicoDrive (in Memory.cpp).\r |
353 | \r |
354 | \r |
355 | Almost there - Reset the 68000!\r |
356 | -------------------------------\r |
357 | \r |
358 | Next we need to Reset the 68000 to get the initial Program Counter and Stack Pointer. This\r |
359 | is obtained from addresses 000000 and 000004.\r |
360 | \r |
361 | Here is code which resets the 68000 (using your memory handlers):\r |
362 | \r |
363 | MyCpu.srh=0x27; // Set supervisor mode\r |
364 | MyCpu.a[7]=MyCpu.read32(0); // Get Stack Pointer\r |
365 | MyCpu.membase=0;\r |
366 | MyCpu.pc=MyCpu.checkpc(MyCpu.read32(4)); // Get Program Counter\r |
367 | \r |
368 | And that's ready to go.\r |
369 | \r |
370 | \r |
371 | Executing the 68000\r |
372 | -------------------\r |
373 | \r |
374 | To execute the 68000, set the 'cycles' variable to the number of cycles you wish to execute,\r |
375 | and then call CycloneRun with a pointer to the Cyclone structure.\r |
376 | \r |
377 | e.g.:\r |
378 | // Execute 1000 cycles on the 68000:\r |
379 | MyCpu.cycles=1000; CycloneRun(&MyCpu);\r |
380 | \r |
381 | For each opcode, the number of cycles it took is subtracted and the function returns when\r |
382 | it reaches 0.\r |
383 | \r |
384 | e.g.\r |
385 | // Execute one instruction on the 68000:\r |
386 | MyCpu.cycles=0; CycloneRun(&MyCpu);\r |
387 | printf(" The opcode took %d cycles\n", -MyCpu.cycles);\r |
388 | \r |
389 | You should try to execute as many cycles as you can for maximum speed.\r |
390 | The number actually executed may be slightly more than requested, i.e. cycles may come\r |
391 | out with a small negative value:\r |
392 | \r |
393 | e.g.\r |
394 | int todo=12000000/60; // 12Mhz, for one 60hz frame\r |
395 | MyCpu.cycles=todo; CycloneRun(&MyCpu);\r |
396 | printf(" Actually executed %d cycles\n", todo-MyCpu.cycles);\r |
397 | \r |
398 | To calculate the number of cycles executed, use this formula:\r |
399 | Number of cycles requested - Cycle counter at the end\r |
400 | \r |
401 | \r |
402 | Interrupts\r |
403 | ----------\r |
404 | \r |
405 | Causing an interrupt is very simple, simply set the irq variable in the Cyclone structure\r |
406 | to the IRQ number.\r |
407 | To lower the IRQ line, set it to zero.\r |
408 | \r |
409 | e.g:\r |
410 | MyCpu.irq=6; // Interrupt level 6\r |
411 | MyCpu.cycles=20000; CycloneRun(&MyCpu);\r |
412 | \r |
413 | Note that the interrupt is not actually processed until the next call to CycloneRun,\r |
414 | and the interrupt may not be taken until the 68000 interrupt mask is changed to allow it.\r |
415 | \r |
416 | ( The IRQ isn't checked on exiting from a memory handler: I don't think this will cause\r |
417 | me any trouble because I've never needed to trigger an interrupt from a memory handler,\r |
418 | but if someone needs to, let me know...)\r |
419 | \r |
420 | \r |
421 | Accessing Cycle Counter\r |
422 | -----------------------\r |
423 | \r |
424 | The cycle counter in the Cyclone structure is not, by default, updated before\r |
425 | calling a memory handler, only at the end of an execution.\r |
426 | \r |
427 | *update*\r |
428 | Now this is configurable in config.h, there is no 'debug' variable.\r |
429 | \r |
430 | \r |
431 | Accessing Program Counter and registers\r |
432 | ---------------------------------------\r |
433 | \r |
434 | You can read Cyclone's registers directly from the structure at any time (as far as I know).\r |
435 | \r |
436 | The Program Counter, should you need to read or write it, is stored with membase\r |
437 | added on. So use this formula to calculate the real 68000 program counter:\r |
438 | \r |
439 | pc = MyCpu.pc - MyCpu.membase;\r |
440 | \r |
441 | The program counter is stored in r4 during execution, and isn't written back to the\r |
442 | structure until the end of execution, which means you can't read normally real it from\r |
443 | a memory handler.\r |
444 | \r |
445 | *update*\r |
446 | Now this is configurable in config.h, there is no 'debug' variable. You can even enable\r |
447 | access to SR if you need. However changing PC in memhandlers is still not safe, you should\r |
448 | better clear cycles, wait untill CycloneRun() returns and then do whatever you need.\r |
449 | \r |
450 | \r |
451 | Emulating more than one CPU\r |
452 | ---------------------------\r |
453 | \r |
454 | Since everything is based on the structures, emulating more than one cpu at the same time\r |
455 | is just a matter of declaring more than one structures and timeslicing. You can emulate\r |
456 | as many 68000s as you want.\r |
457 | Just set up the memory handlers for each cpu and run each cpu for a certain number of cycles.\r |
458 | \r |
459 | e.g.\r |
460 | // Execute 1000 cycles on 68000 #1:\r |
461 | MyCpu.cycles=1000; CycloneRun(&MyCpu);\r |
462 | \r |
463 | // Execute 1000 cycles on 68000 #2:\r |
464 | MyCpu2.cycles=1000; CycloneRun(&MyCpu2);\r |
465 | \r |
466 | \r |
467 | Thanks to...\r |
468 | ------------\r |
469 | \r |
470 | * All the previous code-generating assembler cpu core guys!\r |
471 | Who are iirc... Neill Corlett, Neil Bradley, Mike Coates, Darren Olafson\r |
472 | and Bart Trzynadlowski\r |
473 | \r |
474 | * Charles Macdonald, for researching just about every console ever\r |
475 | * MameDev+FBA, for keeping on going and going and going\r |
476 | \r |
477 | \r |
478 | -------------\r |
479 | \r |
480 | Dave - 17th April 2004\r |
481 | notaz - 17th July 2006\r |
482 | \r |
483 | Homepage: http://www.finalburn.com/\r |
484 | Dave's e-mail: dev(atsymbol)finalburn.com\r |
485 | Replace (atsymbol) with @\r |