Exceptions...

Started by
60 comments, last by null_pointer 23 years, 10 months ago
quote:Original post by Kylotan

No, but it''s quicker and clearer to make certain parts of your program safe with auto_ptr than with new and delete.


OK, I''ll go with faster, slower code.


quote:Original post by Kylotan

You were implying that everything would be accessed through virtual functions in the base class rather than by downcasting to the derived class. I think your original post on this was not very clear so I''m sorry for the misunderstanding.


Hmm...no, if I said something that indicated that, then I''m sorry. I change my mind a lot (who would know?)


quote:Original post by Kylotan

quote:
--------------------------------------------------------------------------------

That is the whole purpose of using abstract classes -- you do not need to know the type, only whether a given class is derived from the abstract base class. Then why should you need to know the type when loading the classes?
--------------------------------------------------------------------------------

I am probably missing your point, but -something- has to know what type it is. Otherwise you cannot allocate the memory and call the correct constructor.


Yes, and that someone is the user of the library, not the library. That''s why I said that using class factories seemed to be transferring the responsibility from the user to the library writer unnecessarily. The user should load his/her classes from disk, not the library.


quote:Original post by Kylotan

&lttoo much to list -- everything and anything about assertions>


Actually, all of the stuff that Eiffel has implemented that you mentioned regarding assertions could probably be done in C++. (You won''t get those pretty blue words, though.) I''ll concede that assertions do have a use, after all. I haven''t used them myself, because most debuggers allow you to run to a certain point in a program and then break into the code. Plus I step through most of my code at least once to make sure my logic is correct. I do disagree with much of MFC but thought it a typical example.

"In properly written code..." -- hmm...interesting...I thought it was subjective?


quote:Original post by Kylotan

quote:
--------------------------------------------------------------------------------

1) When you save the variables into the exception object, you copy by value. So it doesn''t matter where the variables are as they are saved from the calling code, which is -in- the try block (it has to be in the try block to throw()).
--------------------------------------------------------------------------------

Many bugs are down to obscure low-level errors, such as going over an array''s bounds, or a pointer to the wrong place. To call copy constructors on these potentially defective objects (after all, your program has just done something ''wrong'') is not always going to work, when it does it is not always going to be meaningful, and it is not always going to show you the problem.

You are also requiring a heavy handed approach to debugging: grab every variable we could possibly need to check and throw it down. How is this much better than the old routine in Basic of polluting the program with a load of ''print'' statements? How much work do you have to go to for that? It looks like an extremely unwieldy exception class, too. How would you implement that? Variable argument list? Or have to redefine the exception type each time you felt like checking different variables? Or every time you add a new variable to your routine that throws the exception?


1) It''s not a brute force approach -- it''s really rather elegant. Call exception::display() at the point of error, and it works just like an assertion. In circumstances where this is not plausible (fullscreen DX) you use logging, and this is where the variables might be good. Use ltoa() etc. in the constructor of the exception class and then just store the strings. Or use templates. Work smarter, not harder. (well, that''s partially correct)

2) I doubt anyone would write something like this pseudocode:


void do_something(object* p)
{
if( p == NULL )
throw exception(p->data);
}



Don''t you? Even if they did, it would cause an access violation and they would break into the code.

3) This is an example of a good use of those variables:

class direct_x
: public exception
{
public:
direct_x(HRESULT error_code, bool log_to_file)
: error_code(error_code), log_to_file(log_to_file) {};

virtual void display()
{
if(log_to_file)
log();
else
exception::display();
}

protected:
void log() { /* log to a file */ };

HRESULT error_code;
bool log_to_file;
};



Even if you catch it as an exception* and call display(), it still logs to a file. With this method, you would need only a few exception classes, and perhaps derive from direct_x to give more descriptive output.


quote:Original post by Kylotan

quote:
--------------------------------------------------------------------------------

2) How do you get to the information in calling functions from inside called functions when an assertion is called? You hit the break button on the compiler. Do the same when the message box is up in a catch() statement.
--------------------------------------------------------------------------------

Again, by that point several automatic variables have been deallocated, and merely copying them is not always sufficient. You are also suggesting that every function which can ''throw'' has its own ''catch'' block, which is not really the case, nor should it need to be. A 3rd, platform dependent, argument, is that I run programs outside of the debugger and only run the debugger once it has crashed/failed an assertion, by clicking the ''debug'' button that appears. That does not appear with exceptions.

You control where the message box pops up. You could just as easily call display() before throwing the function, so that you can break inside the try() block.

It''s a lot of code and effort compared to assert(a != 0). And doesn''t gain you very much. Cleanup, maybe, but I''ve never found that to be a problem during debugging.


1) You can choose, in your code, to display the exception as an assertion if you like!

2) You don''t have to have a catch statement in every function that has a try block.

3) It may be a lot of code, but Eiffel requires a lot of code too (behind the scenes). Encapsulate it with objects if you like.

4) Doesn''t gain me very much!?!?! *faints*


quote:Original post by Kylotan

Yes, and will often take an order of magnitude less time to write. On exactly the same token, software written in C++ does not run as fast as software written in assembly for exactly the same reasons: C++ is generic and does not take advantage of very specific asm instructions. You lose performance to gain in coding time and structure.

...

It is also like giving power tools to a 4 year old. Once past the low level details, you only need a subset of the functions to actually make the game. The rest can be implemented in something higher level, where you trade off a little performance for more productivity, fewer bugs, and a smaller, more well defined interface. You also do not have to own different compilers for every platform you are aiming at, since the VM is already there. Nor worry about functions that are not present on certain platforms, or require different syntax on different platforms. You can create a slimlined interface that inreases productivity.


1) Hmm...you need a higher level language because it will take less time to write and be faster, because of course C++ is faster to write in and thus slower because its easier and less specific than a higher level language like asm, and VMs are better because they are higher level than C++. ?? I''ll try again. VMs are good because they are higher level than C++ which is higher level than asm, and VMs have little or no performance impact which they should... ??? Please re-read that first paragraph that I quoted -- it makes no sense either with the rest of your arguments. I think I understand you to say that the trend is toward specific/generic software? No, that''s not it... I''ll keep trying...

2) Whoa. WHOA! Back up a little bit. There are some implications here that are incorrect. Let''s compare writing a VM and its implementations to writing a library and its implementations.


    &ltcomparison segment="VM writers">
    The VM writer(s) must own compilers for the target platforms. The VM writer(s) must worry about functions that are not present on certain platforms, and different syntax on different platforms. The VM writer(s) must design a slimlined interface that increases productivity.
    &ltcomparison>

    &ltcomparison segment="library writers">
    The library writer(s) must own compilers for the target platforms. The library writer(s) must worry about functions that are not present on certain platforms, and different syntax on different platforms. The library writer(s) must design a slimlined interface that increases productivity.
    &ltcomparison>

    &ltcomparison segment="VM users">
    The VM user(s) must learn a new language subset.
    &ltcomparison>

    &ltcomparison segment="library uers">
    The library user(s) must learn a new language subset.
    &ltcompatison>



Big difference, huh?

You can see that the differences lay not in the tasks to be done but their difficulty.Which is easier to write? A VM or a few DLLs? You say there are only two differences between the VM and the DLLs, and they are: a) implementations, b) language subsets.

a) The implementation of a DLL is fairly straitforward. Once you have planned the interface classes, you only have to fill in the code for each platform. It''s that simple. The implementation of a VM is much harder to write. You must design your own scripting language, including all the basic commands, and you must write a text parsing engine, an emulator, and then you must still fill in all the same code for the implementations on each of the platforms.

b) The language subsets produced are going to differ. Personally, I think it is the job of the programmer to implement the ideas of the designer, and not to design a system so that the designer can implement his own ideas. I think that is a responsibility of communication and does not call for a VM (design document....design document...) If a group can''t communicate, they''re not going to be able to write a good VM, so it''s pointless to create a VM to handle communication difficulties. If the programmer will be using the language, then it is (obviously) much more efficient to use the DLL approach.


I think that it''s much easier to write a generic class than a text parser/command interpreter. At least to me. Other people may indeed find it easier to write a VM than a class. Poor guys... *sniffles*

Yeah, I trust the industry experts about as far as I can throw them. Remember a little discrepancy back in the late 15th century? An idiot named Columbus thought the world was round. Fancy that. Everyone told him what the trend was. He just wouldn''t follow the popular thinking of the day... Poor guy... What I''m saying is that it''s quite possible for the majority to be wrong, and I think programmers (especially game programmers) have a tendency to jump off the deep end with work-arounds. All the time. I''ve seen implementations for VMs where the "pros" claimed syntax-checking and the like were mere annoyances and should be removed. I''ve seen many benefits that C++ provides re-implemented time and time again in the name of progress. And each time the re-implementation is infinitely worse than the original. It''s terrible. What''s worse is that newbies kiss the ground these experts walk on. The whole thing is kind of silly if you ask me.

The point of this discussion is that most of the advantages/disadvantages you listed have absolutely no bearing on VMs vs. DLLs. Conceptually, they''re the same: handled by the writer and hidden from the user. In short, libraries using abstract base classes provide all the scripting language you need while maintaining the things that SHOULD be standard, like memory management, syntax, etc. You gain benefit (in programmer time) from things being standard, and others being specific. That''s why we have APIs and the base language. The base language provides some things that are common to all programs, while APIs provide things specific to each type of program. It''s like an abstract class hierarchy -- some things are common and are placed in the base class, and some things are specific and placed in the derived classes.

Libraries using abstract base classes are the functional equivalent of a VM. However, libraries are easier to write; easier to maintain; easier to learn. There remains no justification for using a VM. Or is there? There are only two relevant points that you brought up concerning VMs: 1) portability, and 2) use by non-programmers. Personally, I think they are both irrelevant, but we''ll see.


#1 PORTABILITY

I argued against this last time, and the only thing you came up with is that you do not need a compiler for every target platform. I''m sorry, but that is what compilers were made to do. It''s what they do best; better than a lot of programmers; better than a VM. The time taken to write a VM could have been spent earning money to get the target platforms and compilers (most are either cheap or free). And you''d have a better game to distribute. Doh! Also, as I said before the VM writers must still have a compiler for every platform on which the VM will run.


#2 SCRIPTING

I already said before that VMs are just a work-around for what libraries already provide. Now you say that non-programmers should be able to write the game, to eliminate miscommunication between the programmer and the designer. I say that the benefit gained from any language is proportional to the experience in programming; the designers who do not program cannot produce as efficient a game as the programmers themselves. It is the responsibility of the programmer, the design documents, and the designer to iron out these things beforehand or set up a system whereby that might be easily accomplished. Don''t apply the band-aid of letting the final project suffer because of miscommunication. VMs are not cure-alls. Letting the designer code the game is just a poor work-around and as such an unnecessary waste of time. (BTW, A good program is flexible and extensible, and you should have no trouble adding features to it.)


Designers design; programmers program; compilers compile; virtual machines...?


quote:Original post by Kylotan

Why should an API differ from the language?


*blinks*

Why should software differ from the operating system? Why should the operating system differ from the BIOS? Why should the BIOS differ from the chip on which it is burned? Why are CPUs sold separate from the motherboard? Why use classes and functions? Why not assembly? Why classify and separate things at all?

Now you are you arguing against encapsulation. This is madness!


quote:Original post by Kylotan

If you want to deny what is -actually- happening, that is fine. But the real, observed trend is -away- from native code and towards scripting languages and engine-specific languages.


STL? no, that''s too generic and slow. DX? nah, not specific-enough. Platform-independent APIs (OpenML) founded and supported by large companies like Intel? nah, too far away from the problem domain...etc. If you want to deny the trend, then go right ahead.

People don''t always observe things correctly. And programmers are known for huge, inefficient work-arounds for tiny problems. Just because a solution requires more work and/or more intelligence doesn''t mean it is better than a simple solution. You said yourself that of two solutions, the simpler one is almost always better.

BTW, what is nested quoting?



- null_pointer
Sabre Multimedia
Advertisement
quote: Original post by null_pointer
OK, I'll go with faster, slower code.

Faster to write, slower to execute. Profile and optimize later if necessary.

quote:"In properly written code..." -- hmm...interesting...I thought it was subjective?

Yes. We were going by my rules here

Seriously, I think the main use for assertions is in testing your own logic, and should never be in testing someone else's. It is like scaffolding that builders use: it holds the building up during construction, but is not meant to be there when you're done, and certainly shouldn't be relied upon by anyone else wishing to use the building.

quote:
1) It's not a brute force approach -- it's really rather elegant. Call exception::display() at the point of error, and it works just like an assertion.


I have a concession to make: if you set Visual C++ to break on exceptions, you can probably use them to get at the call stack just as effectively as using an assertion.

However, this doesn't counter the issues of exceptions causing code bloat and slowdown, or of being more long-winded to use in simple situations.

quote:
2) I doubt anyone would write something like this pseudocode:


void do_something(object* p)
{
if( p == NULL )
throw exception(p->data);
}


Don't you? Even if they did, it would cause an access violation and they would break into the code.


No, but if p == NULL, they may want to see q->data, or some other variable that is there. A lot of my functions have too many variables to output using a Display() function.

quote:
2) You don't have to have a catch statement in every function that has a try block.


No, but if you caught the exception further down the stack, you've already lost several stack-based variables which might have been important for debugging. You often need to stop execution at the first point that it becomes a problem for debugging purposes. I accept that in release mode, your priorities shift to having some sort of graceful exit.

quote:
3) It may be a lot of code, but Eiffel requires a lot of code too (behind the scenes). Encapsulate it with objects if you like.


You can't really encapsulate exception handling in an object. Maybe with macros. What are you going to do to create something as concise as 'assert(x != NULL)'? What's the point of encapsulating this single line of code?

quote:1) Hmm...you need a higher level language because it will take less time to write and be faster, because of course C++ is faster to write in and thus slower because its easier and less specific than a higher level language like asm, and VMs are better because they are higher level than C++. ??


Faster to code, slower to execute. In cases where execution speed is not critical, this is a very useful tradeoff to be able to make.

quote:
You can see that the differences lay not in the tasks to be done but their difficulty.Which is easier to write? A VM or a few DLLs? You say there are only two differences between the VM and the DLLs, and they are: a) implementations, b) language subsets.


Well, you are still looking at everything from a programmer's perspective, and only the original game programmer. Sure, the VM is going to be harder to write, but gives different rewards, rewards which are better in many cases.

quote:The implementation of a VM is much harder to write. You must design your own scripting language, including all the basic commands, and you must write a text parsing engine, an emulator, and then you must still fill in all the same code for the implementations on each of the platforms.


Well, designing a language is not hard generally. The way it seems to work is that people who have worked on many games before get to know exactly what kind of things their language would need. And then add those features to a standard syntax, while stripping out anything they don't need. UnrealScript, for instance, features 'state' where a class can have different functions executed depending on their state. It's an encapsulation of a switch statement enforced by code and made simpler to understand. So, they put that into the language. It didn't require much thought, I assume: they were aware of the need for it, and just did it.

quote:
Personally, I think it is the job of the programmer to implement the ideas of the designer, and not to design a system so that the designer can implement his own ideas.

You are basically saying "Don't empower designers, hire more programmers." Are you out of work?

In a traditional setup, a designer's role might get less and less important as the game progresses. This is a waste of a valuable human resource. Would you just teach them C++ and get them to augment the coding team, even though it would probably take too long to get them to a standard where they can make 'production-quality code'?

Secondly, you seem to be thinking of a designer as "The Idea Guy". This really is not the role that they assume in most games these days. The design team will often be involved in producing the maps or levels. This includes deciding mechanical functionality, whether it is some complex puzzle, or an enemy reaction to you entering a certain area, or that a lift lowers when you flick a certain lever. Chances are, they may write some of these things in a very high level type of pseudocode anyway. Instead of writing down "When lever 1 is thrown, start lift 8 up and release the zombie from the room next to it", a scripting language allows them to write:
Lever1::OnThrownLift(8).StartDoor(1).OpenZombie(3).WakeEnd  

Now yes, all the above could be done in C++ in some form. Do we want to require designers to have to use Visual C++ to test a 3-line piece of code? Or should the designer have to write that down, pass it to a programmer, wait for it to be implemented (at the end of a long queue of every other designers' requests,no doubt) and then test it? What if it didn't work? Rewrite the 3 line script, resubmit to programmer, rewait? If I was a designer I'd tear my hair out at that. I'd want to be able to quickly test that myself. After all, it doesn't look too difficult, even to a non programmer. ("Before the dot is the name of your thing, after the dot is what you want your thing to do.") Instead of requiring a C++ compiler, why not just have a list of events and a text editor in the level editing program, and store the scripts with the level? And why compile the above, when interpreting it would take next to no time?

Given the right tools, a designer can produce 'production quality' code that fulfills their aims and allows them to work in parallel with programmers, rather than waiting on them for basic tasks. That quality of code is far easier to create in a language that doesn't allow for memory leaks, that provides a simplified container class, that doesn't require knowledge of separate compilation and linking steps, etc.

quote: If the programmer will be using the language, then it is (obviously) much more efficient to use the DLL approach.

Not if they don't want to have to relink the project every time they tweak 3 lines of code...

quote:
I think that it's much easier to write a generic class than a text parser/command interpreter. At least to me. Other people may indeed find it easier to write a VM than a class. Poor guys... *sniffles*

Parsers are quite simple to write if the language suits them. C++ is hard to write a parser for, whereas UnrealScript or Visual Basic are easy. This is down to things such as preceding function names with the keyword 'function' that immediately switches the parser into "Oh, so I'm expecting a function" mode, rather than "hmm, I just read 'int'... is this a function definition, a function prototype, a variable declaration?"

I don't know how easy or difficult the memory management side of things would be. But since nearly all scripting languages either have (a) no concept of the free store, or (b) garbage collection, I doubt it would be too difficult. Anything on the stack just disappears when it goes out of scope. Anything else is reference counted. Simple enough, I guess.

quote:Yeah, I trust the industry experts about as far as I can throw them.


Well, I wasn't saying "The Big Guys do this, so it must be right." But in this case, I think they are. The ones with the vision are empowering their entire team, rather than forcing some sort of tree structure where everything has to go through the programmers at the root. They're distributing the high level tasks out among those who can handle them, and working on the low level stuff that no-one else can deal with.

quote:
I've seen implementations for VMs where the "pros" claimed syntax-checking and the like were mere annoyances and should be removed.


Well, in some applications, they are mere annoyances. The benefits of type checking in some systems are far outweighed by the extra effort needed in having to declare all your variables explicitly. It sometimes makes more sense to just execute anyway, and log any 'errors' during execution, rather than to refuse to run at all.

quote:The point of this discussion is that most of the advantages/disadvantages you listed have absolutely no bearing on VMs vs. DLLs. Conceptually, they're the same: handled by the writer and hidden from the user.


The distinction is: what kind of user? Do you want all your 'users' to be proficient in C++? If so, you are cutting out a lot of potential users.

quote: In short, libraries using abstract base classes provide all the scripting language you need while maintaining the things that SHOULD be standard, like memory management, syntax, etc.


Why should a designer who just wants 3 zombies to appear at the flick of a switch need to know about memory management?

quote:You gain benefit (in programmer time) from things being standard, and others being specific.


You also gain in programmer time by having a language that is more suitable to your application. There are some things I can't imagine doing without pointers. But on the other hand, Visual Basic's Collection object is far cleaner than C++'s std::map<>, while performing much the same task. No single language is perfect for everything. Therefore it may make sense to have 2 languages for 1 project: one for high level logic, one for low level number crunching.

Some projects will benefit from their own language, and some will not. This is a design decision that should not be taken lightly. Unless you are doing it as a learning experience, of course. Many projects would do well by simply embedding an existing scripting language, gaining many of the benefits I have detailed already, with less overhead than writing a dedicated VM.

quote:
#1 PORTABILITY

I argued against this last time, and the only thing you came up with is that you do not need a compiler for every target platform. I'm sorry, but that is what compilers were made to do.


What kind of argument is that? Guns are made to kill things: does this mean we should go out shooting people just because guns exist? There are often better ways of resolving conflict than to shoot people. And there are often better ways of producing a game than compiled native code.

And you are adding an extra burden on people who want to work on your game: they have to buy an extra product that costs quite a lot. This may be fine in-house, but many people write their games specifically to appeal to the mod community, which increases their game's longevity and interest. If you require Visual C++ to make a mod, fewer people are interested. Sure, you can get DJGPP but that is hardly easy to use and has no decent IDE to speak of. The Borland compiler also exists, but again, text mode is not all that simple to use and is an added complication.

Yes, the VM writers will already have a compiler, but I am assuming that one central reason for writing a VM is so that other people can contribute to the project, not merely the same people who worked on the VM.

quote:
I say that the benefit gained from any language is proportional to the experience in programming; the designers who do not program cannot produce as efficient a game as the programmers themselves.


Sure. But the difference is in what that proportion is. Double the programming experience and you double the output. But take 2 people with equal programming knowledge down, 1 given ASM, and the other given Java, and see who can make something meaningful first. Double their experience level, and the Java guy will still be far more productive than the ASM person.

Just because the designer cannot do it as well as a programmer could, doesn't mean they can't do it well enough to be shippable, especially if the programmer has created a good environment in which bugs are next to impossible. if we always leave everything to the best person, then nothing will ever get done. We would have nobody entering the industry as they would be pointless: after all, a senior programmer can do everything a junior one can. It doesn't work that way. You have to make good use of everyone's time.

Maybe a script to start a lift moving on the flick of a switch takes 10ms to execute, and native code would take 1ms. Will the game player care? They may be more interested in the fact that the levels are more intricate due to designers being more able to express their creativity.

quote:Letting the designer code the game is just a poor work-around and as such an unnecessary waste of time. (BTW, A good program is flexible and extensible, and you should have no trouble adding features to it.)


It saves programmer time, and allows better use of designer time. It removes the delay between a designer realising a feature is needed and the programmer implementing that feature.

quote:
Why should software differ from the operating system? Why should the operating system differ from the BIOS? Why should the BIOS differ from the chip on which it is burned? Why are CPUs sold separate from the motherboard? Why use classes and functions? Why not assembly? Why classify and separate things at all?

Now you are you arguing against encapsulation. This is madness!


Tell me... do you never use anything like this in your code:
int main(){    try    {    Application.Run();    }    catch(...)    {    // blah    }}  

Of course, the Application has lots of encapsulation between its parts. All subsystems are encapsulated, I assume. But the outside world only ever needs to see one interface. The Application object itself takes care of calling Init(), of entering a loop of calls to OnIdle(), and Shutdown() at the end.

Encapsulation isn't just about separating things, it's about putting separate things in one place with a standard interface. Hence the origin of the word: to put within a capsule.

Many other languages work just fine without you ever needing to know about the linker, or a compiler, or needing to declare forward references, or any of that. This can all be done for you. The reason C++ doesn't do it is down to C compatibility and the desire to not harm performance. If you are willing to trade away performance for a lot more simplicity, you don't need all these details. Scripting actions for a game is a prime candidate for this.

quote:
STL? no, that's too generic and slow. DX? nah, not specific-enough. Platform-independent APIs (OpenML) founded and supported by large companies like Intel? nah, too far away from the problem domain...etc. If you want to deny the trend, then go right ahead.


Everything you just mentioned is relatively low level. And routinely get used as part of the construction of the engine side of the game. But they are not -adding- these things to their projects, they are using them to replace what used to be platform specific code at the low level. In fact, the use of such libraries generally means they are using less code themselves to achieve the same effect. It took me a long time to get D3D initialisation working, and now I just downloaded a zip file from Witchlord's site that does the same thing, via D3DX, in about 1/20 of the code. Sure, this is a library rather than a VM, but the point here was that there is less code on my behalf to achieve the goal, and the goal here is the creation of the low-level half of the game that is hardware-based.

There is another half, where the direction is also away from native code, but not towards libraries. It's towards writing engines extensible by more than just hardcore programmers, that are useful in more than 1 style of game. The best way of achieving this is probably to use scripting or a VM in some form. The 2-tier approach to software is growing. It's getting harder to find games -without- scripting in them somewhere.

quote:Just because a solution requires more work and/or more intelligence doesn't mean it is better than a simple solution. You said yourself that of two solutions, the simpler one is almost always better.


Right. But this is about distributing effort more evenly. In the early 80s, nearly all games were written by ASM programmers, except for a few slow monoliths carved out in BASIC. As games have become more advanced, they are done in progressively more high-level languages. And therefore more people are able to contribute. In order to make most effective use of resources, a team would want to be able to
draw on the expertise of non-programmers. So the idea is that the programmers make the 'difficult' solution as it provides 'simple' solutions for non-programmers. The end result being more productivity overall. Case study: go out and count how many lines of code have been written in UnrealScript mods. Could the original Unreal programmers have written -all- that code in the time it took them to write the VM? I doubt it. By creating the tool, they could channel a lot of productivity that would otherwise have lain dormant. It was a short term 'expense' that led to long term rewards.

Edited by - Kylotan on June 6, 2000 7:59:41 AM

This topic is closed to new replies.

Advertisement