Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.

Due to spam on this forum, all posts now need moderator approval.

 Entire forum ➜ SMAUG ➜ SMAUG coding ➜ Processing Parenthesis Correctly

Processing Parenthesis Correctly

It is now over 60 days since the last post. This thread is closed.     Refresh page


Pages: 1 2  3  

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Thu 07 Dec 2006 02:02 PM (UTC)
Message
I am trying to make it so my scripting system will process internal parenthesis first, but I am having a hard time, with the loop, data keeps getting lost somewhere. Here is an example of what I am having my prog function process:

setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))


    int left = 0, z, cur;

    cmnd[0] = '\0';

    for ( x=0; parse[x] != '\0'; x++ )
    {
	if ( parse[x] == '(' )
	    left++;
    }

    if ( left > 0 )
    {
	char *funcs[left];
	char func[MIL];
	bool infunc = FALSE;
	int marker = left;

    	while ( marker > 0 )
    	{
	    cur = 0;
	    func[0] = '\0';
	    infunc = FALSE;

	    for ( z = 0; parse[z] != '\0'; z++ )
	    {
		if ( infunc )
		{
		    if ( parse[z] == ')' )
		    {
			funcs[marker] = parse_setvar( func, mob, actor, obj, vo, rndm );
			break;
		    }
		    else
		    {
			add_letter( func, parse[z] );
			continue;
		    }
		}

	    	if ( parse[z] == '(' )
		    cur++;

	    	if ( cur == marker )
	    	{
		    infunc = TRUE;
	    	}
	    }
	    marker--;
	}

	cur = 0;

	for ( z = 0; z < strlen(parse); z++ )
	{
	    if ( parse[z] == '(' )
	    {
		int a;

		for ( a = 0; funcs[cur][a] != '\0'; a++ )
		{
		    add_letter( cmnd, funcs[cur][a] );
		}
		z += strlen(funcs[cur]);
		cur++;
	    }
	    else
	    {
		add_letter( cmnd, parse[z] );
	    }
	}
    }
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #1 on Thu 07 Dec 2006 06:55 PM (UTC)
Message
Could you give an example of the data loss? And what exactly are your language specifications? Do you allow arbitrary nested expressions? Just one?

This kind of thing reminds me why I'm so unhappy with the mudprog language. Sigh.

I would approach this problem with recursion, such that each opened parenthesis would be a new recursive call. That way all this stuff takes care of itself instead of having to implement this messy and error-prone iterative solution.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #2 on Thu 07 Dec 2006 10:24 PM (UTC)
Message
The language is C, its not an independent scripting system, i am just used to calling the mud progz from smaug scripts. everything is written in smaug for smaug, which is why i posted on the smaug board. i put a debug check in and the loss is a little less than i had thought at first:


setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))

becomes:

setvar $n Dende.KillBoars $f.increment(n.getvarDende.KillBoars))


I also put in a couple debugs in the actual functions themselves. Apparently the GETVAR function is being called, however the only thing being sent to the function is GETVAR, and nothing else:

[PROGS] GetVar: Word = getvar, Mob #27412.
[PROGS] Function GETVAR missing opening parenthesis(, Mob #27412.

The part: parenthesis(

Is supposed to be: parenthesis(%c), but since only GETVAR is being sent that specific point is the '\0' char.

When all is said and done I want the following to happen:


setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))

becomes:

setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))

becomes:

setvar $n Dende.KillBoars $f.increment(10)

becomes:

setvar $n Dende.KillBoars 11
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #3 on Thu 07 Dec 2006 11:03 PM (UTC)
Message
Ok I went back and redid my code so that a single parenthesis is evaluated in each pass of parse_setvar, I am really close to this being right:

setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))

now becomes:

setvar $n Dende.KillBoars $f.increment(Dende.KillBoars0))

Good news, GETVAR is finally being evaluated. The 0 is correct, however the first part shouldn't be going back there and neither should one of those closing parenthesis. Here is the new code.


  if ( !str_infix( "(", parse ) )
  {
    int left = 0, z, right = 0;
    char func[MIL];
    bool infunc = FALSE, skipToRightParen = FALSE;

    cmnd[0] = '\0';

    for ( x=0; parse[x] != '\0'; x++ )
    {
        if ( parse[x] == '(' )
            left++;
    }

    func[0] = '\0';
    infunc = FALSE;

    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( infunc )
        {
            if ( parse[z] == ')' )
            {
                right++;
                if ( right == left )
                {
                    sprintf( func, "%s", parse_setvar( func, mob, actor, obj, vo, rndm ) );
                    break;
                }
                add_letter( func, parse[z] );
                continue;
            }
            else
            {
                add_letter( func, parse[z] );
                continue;
            }
        }

        if ( parse[z] == '(' )
        {
            infunc = TRUE;
        }
    }

    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( skipToRightParen && parse[z] != ')' )
            continue;

        if ( parse[z] == '(' )
        {
            int a;

            add_letter( cmnd, parse[z] );

            for ( a = 0; func[a] != '\0'; a++ )
            {
                add_letter( cmnd, func[a] );
            }
            skipToRightParen = TRUE;
        }
        else
        {
            add_letter( cmnd, parse[z] );
        }
    }
  }
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #4 on Fri 08 Dec 2006 12:35 AM (UTC)
Message
Quote:
The language is C, its not an independent scripting system, i am just used to calling the mud progz from smaug scripts. everything is written in smaug for smaug, which is why i posted on the smaug board.
This statement makes no sense to me which makes me think we were talking about completely different things. My comment was that the SMAUG mudprog language is very annoying to work with and is turning into a monstrous hack as people want more and more features. The implementation of if-checks, for example, is complicated beyond belief given how easy it should be.

Basically it is a language that is the result of people who don't know how to write compilers who try to hack together a compiler, and as a result there are mistakes, unnecessary complications, weirdnesses and all around bad times.


For example, what you are trying to do here should be trivial but due to the bad way these programs are implemented has turned out to be quite difficult.

Again, I would try implementing this with recursion so that nested parentheses are handled more naturally. Are you familiar with writing recursive functions?

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #5 on Fri 08 Dec 2006 02:33 AM (UTC)
Message
I am not too sure what a recursive function is, but I thought thats what I was doing, as this code is the beginning of my parse_setvar function, so the code to handle the parenthesis is calling the same function it is in, hence recursive.


parse_setvar: setvar $n Dende.KillBoars $f.increment($n.getvar(Dende.KillBoars))
parse_setvar: $n.getvar(Dende.KillBoars)
parse_setvar: Dende.KillBoars

oh and i meant the code i had up was C, at least i think thats what smaug is. the 'setvar' stuff is an addition to the mudprog. whether ot not the mudprog is hacked together, i would still like to get this to work.
Top

Posted by Nick Gammon   Australia  (23,133 posts)  Bio   Forum Administrator
Date Reply #6 on Fri 08 Dec 2006 06:05 AM (UTC)
Message
Quote:

My comment was that the SMAUG mudprog language is very annoying to work with and is turning into a monstrous hack as people want more and more features. The implementation of if-checks, for example, is complicated beyond belief given how easy it should be.


Honestly, I think the whole thing should be converted to Lua, which works reliably. There is probably a conversion issue for existing scripts, not sure how big an issue that would be.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #7 on Fri 08 Dec 2006 06:32 AM (UTC)
Message
Quote:
Honestly, I think the whole thing should be converted to Lua, which works reliably. There is probably a conversion issue for existing scripts, not sure how big an issue that would be.
I agree Nick. It's possible that the conversion wouldn't actually be that bad -- the mudprog language has such primitive syntax that you could probably convert everything automatically. And it's not as if there are huge numbers of scripts, either. The real problem I see is reimplementing all of the functions that mudprog uses to hook into the MUD; they'd have to be "Luified". That hurt the scripting language I implemented; it was getting to be too much work to add all the functions. Knowing what I know now, though, I could have chosen a much better solution, an automatic binding of sorts, but anyhow...

To return to the problem at hand:

Gohan, it's true that technically if a function calls itself, that is recursion, but there's a lot more to it than just calling yourself. You need to be thinking about the problem in a fairly specific way. Recursion takes a big problem, and breaks it into smaller parts, and then solves those smaller parts, using the solutions to create the bigger part's solution.

In your case, if we were to implement this recursively (which I think would be much, much better, mainly because it's more robust [1]) we would need to think carefully about what exactly the problem is, and what exactly the expected behavior is.

[1] for example, your code would not work with functions of several arguments with nested sub-expressions, because you only recurse when you have found the same number of rights as of lefts.

In general, what we are trying to do here is evaluate sub-expressions before evaluating expressions. Since mudprog is all text-based and doesn't actually have program structure trees, we have to replace the sub-expression with some text (the result of the evaluation) and use that in the bigger expression.

Here is how you should be thinking about the problem:

Seeing an open parenthesis means you are entering a new expression context. As soon as you do that, you should leave your old context and work on your next context. As soon as you find the close parenthesis, you can leave your context. Nested expressions are handled naturally, because finding a new open parenthesis entails opening a new context.

Once you leave a context, you may evaluate the sub-expression however you want to, and then replace the result in the greater expression. Unfortunately, C makes that a little harder than it should be, but whatever; the idea is that you would declare the "result" to be everything up until your sub-expression, then the result of evaluating your sub-expression, and then everything from the sub-expression. Creating this dynamic string is a little annoying in C, but so be it.

Anyhow, this kind of problem is very easy to get lost in. (In fact, it's so hard to deal with because it was written by people who probably didn't really understand the issues, and just banged at it until it looked like it worked.) In particular, it's easy to do something that looks like it works but doesn't; in your code, for instance, even if you got it working for your test case you would have made the nasty discovery further along the line that multiple arguments weren't handled.
My suggestion is to be very careful with this. Try to develop a very precise understanding of what you are trying to do. Define what should happen and under what circumstances. If you can't write it in natural language, there is no way you will be able to write it in a programming language.

Incidentally, what you're trying to do isn't really that easy. So don't get discouraged if you have trouble with it. Take your time and proceed with caution.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,133 posts)  Bio   Forum Administrator
Date Reply #8 on Sat 09 Dec 2006 01:57 AM (UTC)
Message
Quote:

The real problem I see is reimplementing all of the functions that mudprog uses to hook into the MUD; they'd have to be "Luified".


I think you could write some "glue" routines fairly easily.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #9 on Sat 09 Dec 2006 04:02 AM (UTC)
Message
Yes, that's what I was referring to regarding my new knowledge. Still it'd be somewhat of a hassle, and the glue routines would be messy. For one, you'd have to enumerate the functions you want glued. OK, no big problem, you have to do that anyhow. But more importantly you'd have to convert the Lua arguments to a string of sorts, give it to the mudprog function, and figure out how to get a result back to Lua.

I worked on something like this over the summer at D. E. Shaw, taking a program that had embedded Tcl and adding embedded Perl as well. (Perl embedding is soooo nasty compared to, say, Lua.) This would be even harder because unlike Tcl, mudprog doesn't have a proper return value structure.

One very useful command would be a "mudprog" command of sorts that would allow Lua to execute an arbitrary string of mudprog. E.g., mudprog("mset david health 200"). The hard part, again, is getting a return value.

In fact, the hardest challenge I faced was not at all running mudprog commands (which was easy: just do what I said above), it was reimplementing all the if-checks. mudprog if-checks aren't implemented as functions, so it's fairly hard to extract a value from them.

I really don't like mudprog. :-(

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #10 on Sat 09 Dec 2006 11:00 AM (UTC)
Message
So do you have any clue as to what is wrong with the code I have posted, any ideas at all.
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #11 on Sat 09 Dec 2006 09:24 PM (UTC)
Message
One thing I already mentioned is that you won't be able to handle multiple pairs of parentheses.

One thing you should check very carefully is how you are reconstructing your string. It's a little hard to know for sure without seeing the function. Also, the method is so confusing that it's very hard to debug in general.

Normally I would be able to write this fairly quickly, but at the moment I'm so busy that I don't time for my own hobbies. :(

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #12 on Tue 12 Dec 2006 01:39 AM (UTC)

Amended on Mon 08 Jan 2007 05:19 PM (UTC) by Gohan_TheDragonball

Message

char *parse_setvar( char *cmnd, CHAR_DATA *mob, CHAR_DATA *actor, OBJ_DATA *obj, void *vo, CHAR_DATA *rndm )
{
  static char results[MSL*2];
  int x;
  CHAR_DATA *chkchar = NULL;
  OBJ_DATA *chkobj = NULL;
  ROOM_INDEX_DATA *chkroom = NULL;
  bool function = FALSE;
  char parse[MIL];
  sprintf( parse, "%s", cmnd );
  results[0] = '\0';

  if ( !str_infix( "(", parse ) )
  {
    int left = 0, z, right = 0;
    char func[MIL];
    bool infunc = FALSE, skipToRightParen = FALSE;

    cmnd[0] = '\0';

    for ( x=0; parse[x] != '\0'; x++ )
    {
        if ( parse[x] == '(' )
            left++;
    }

    func[0] = '\0';
    infunc = FALSE;

    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( infunc )
        {
            if ( parse[z] == ')' )
            {
                right++;
                if ( right == left )
                {
                    sprintf( func, "%s", parse_setvar( func, mob, actor, obj, vo, rndm ) );
                    break;
                }
                else
                    add_letter( func, parse[z] );
                continue;
            }
            else
            {
                add_letter( func, parse[z] );
                continue;
            }
        }

        if ( parse[z] == '(' )
        {
            infunc = TRUE;
        }
    }

    right = 0;
    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( parse[z] == ')' )
            right++;

        if ( skipToRightParen )
        {
            if ( right < left )
                continue;

            skipToRightParen = FALSE;
        }

        if ( parse[z] == '(' )
        {
            int a;

            add_letter( cmnd, parse[z] );

            for ( a = 0; func[a] != '\0'; a++ )
            {
                add_letter( cmnd, func[a] );
            }
            skipToRightParen = TRUE;
        }
        else
        {
            add_letter( cmnd, parse[z] );
        }
    }
  }

  for ( x=0; cmnd[x] != '\0'; x++)
  {
    chkchar = NULL;
    chkobj = NULL;
    chkroom = NULL;
    function = FALSE;
    if (cmnd[x] == '\0')
      return results;
    if (cmnd[x] != '$')
      add_letter( results, cmnd[x]);
    else
    {
       ... section not to do with string manipulation
    }
    if (chkchar || chkobj || chkroom || function)  // Ok, so one of them is valid, lets find out what the info we're looking at is.
    {
      x+=2; // skip over the identifier and go straight into whats next...
      if (cmnd[x] != '.' || cmnd[x] == '\0') //  not a period, so this isnt a pointer, just a variable.
      {
         add_letter( results, cmnd[x-2]); // Put this piece back
         add_letter( results, cmnd[x-1]); // Put this piece back
         add_letter( results, cmnd[x]); // Put this piece back
         if ( cmnd[x] == '\0')
           return results;
         continue; // and keep going.
      }
      else  // Ok, they used the right syntax, lets fight out what our 'word' is.
      {
         char word[MIL];;
         word[0] = '\0';
         x++;
         while ( !isspace(cmnd[x]) && cmnd[x] != '\0' )
         {
            add_letter( word, cmnd[x]);
            x++;
         }
         // If we get here, we assume we now have the word.
         if (chkchar != NULL)  // Begin looking for possible character matches.
         {
             if (!str_cmp( word, "getvar"))
             {
                VAR_DATA *var;
                char arg[MIL];
                bool found = FALSE;
                arg[0] = '\0';
                int y;

                if ( word[6] != '(' )
                {
                    progbug( format( "Function GETVAR missing opening parenthesis(%c)", word[6] ), mob );
                    return results;
                }

                for ( y = 7; y <= strlen(word); y++ )
                {
                    if ( word[y] == ')' )
                        break;

                    if ( word[y] == '\0' )
                    {
                        progbug( "Illegal use of GETVAR function (End of String Reached Before Finding a Closing Parenthesis)", mob );
                        return results;
                    }
                    add_letter( arg, word[y] );
                }

                for ( var = chkchar->first_var; var; var = var->next )
                {
                    if ( !str_cmp( var->name, arg ) )
                    {
                        strcat( results, format( "%s", var->value ) );
                        found = TRUE;
                    }
                }

                if ( !found )
                {
                    progbug( format( "Getvar: Var not found on char(%s)", arg ), mob );
                    strcat( results, "NullVar" );
                }
             }
         }
         if (cmnd[x] == '\0')
          return results;
         else
          add_letter( results, cmnd[x]);
      }
    }
  }
  return results;
}
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #13 on Tue 12 Dec 2006 01:40 AM (UTC)
Message
called in function mprog_do_ifcheck()

doneargs:
    q = p;
    while ( isoperator(*p) ) ++p;
    strncpy(opr, q, p-q);
    opr[p-q] = '\0';
    while ( isspace(*p) ) *p++ = '\0';
    rval = parse_setvar( p, mob, actor, obj, vo, rndm );


in case you haven't enountered it before, the add_letter function

void add_letter( char *string, char letter)
{
  char buf[MIL];

  sprintf(buf, "%c", letter);
  strcat(string, buf);
  return;
}
Top

Posted by Gohan_TheDragonball   USA  (183 posts)  Bio
Date Reply #14 on Mon 08 Jan 2007 05:19 PM (UTC)
Message
i finally had some time off from work and went through and fixed this code, in case anyone was waiting for the fix i commented around the addition to the code that fixes it, the following code is flawlessly working version.


char *parse_setvar( char *cmnd, CHAR_DATA *mob, CHAR_DATA *actor, OBJ_DATA *obj, void *vo, CHAR_DATA *rndm )
{
  static char results[MSL*2];
  int x;
  CHAR_DATA *chkchar = NULL;
  OBJ_DATA *chkobj = NULL;
  ROOM_INDEX_DATA *chkroom = NULL;
  bool function = FALSE;
  char parse[MIL];
  sprintf( parse, "%s", cmnd );
  results[0] = '\0';

  if ( !str_infix( "(", parse ) )
  {
    int left = 0, z, right = 0;
    char func[MIL];
    bool infunc = FALSE, skipToRightParen = FALSE;

    cmnd[0] = '\0';

    for ( x=0; parse[x] != '\0'; x++ )
    {
        if ( parse[x] == '(' )
            left++;
    }

    func[0] = '\0';
    infunc = FALSE;

    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( infunc )
        {
            if ( parse[z] == ')' )
            {
                right++;
                if ( right == left )
                {
                    sprintf( func, "%s", parse_setvar( func, mob, actor, obj, vo, rndm ) );
                    break;
                }
                else
                    add_letter( func, parse[z] );
                continue;
            }
            else
            {
                add_letter( func, parse[z] );
                continue;
            }
        }

        if ( parse[z] == '(' )
        {
            infunc = TRUE;
        }
    }

    right = 0;
    for ( z = 0; parse[z] != '\0'; z++ )
    {
        if ( parse[z] == ')' )
            right++;

        if ( skipToRightParen )
        {
            if ( right < left )
                continue;

            skipToRightParen = FALSE;
        }

        if ( parse[z] == '(' )
        {
            int a;

            add_letter( cmnd, parse[z] );

            for ( a = 0; func[a] != '\0'; a++ )
            {
                add_letter( cmnd, func[a] );
            }
            skipToRightParen = TRUE;
        }
        else
        {
            add_letter( cmnd, parse[z] );
        }
    }
  }

  // Fixed with one simple correction
  results[0] = '\0';
  // End Fix

  for ( x=0; cmnd[x] != '\0'; x++)
  {
    chkchar = NULL;
    chkobj = NULL;
    chkroom = NULL;
    function = FALSE;
    if (cmnd[x] == '\0')
      return results;
    if (cmnd[x] != '$')
      add_letter( results, cmnd[x]);
    else
    {
       ... section not to do with string manipulation
    }
    if (chkchar || chkobj || chkroom || function)  // Ok, so one of them is valid, lets find out what the info we're looking at is.
    {
      x+=2; // skip over the identifier and go straight into whats next...
      if (cmnd[x] != '.' || cmnd[x] == '\0') //  not a period, so this isnt a pointer, just a variable.
      {
         add_letter( results, cmnd[x-2]); // Put this piece back
         add_letter( results, cmnd[x-1]); // Put this piece back
         add_letter( results, cmnd[x]); // Put this piece back
         if ( cmnd[x] == '\0')
           return results;
         continue; // and keep going.
      }
      else  // Ok, they used the right syntax, lets fight out what our 'word' is.
      {
         char word[MIL];;
         word[0] = '\0';
         x++;
         while ( !isspace(cmnd[x]) && cmnd[x] != '\0' )
         {
            add_letter( word, cmnd[x]);
            x++;
         }
         // If we get here, we assume we now have the word.
         if (chkchar != NULL)  // Begin looking for possible character matches.
         {
             if (!str_prefix( "getvar", word))
             {
                VAR_DATA *var;
                char arg[MIL];
                bool found = FALSE;
                arg[0] = '\0';
                int y;

                if ( word[6] != '(' )
                {
                    progbug( format( "Function GETVAR missing opening parenthesis(%c)", word[6] ), mob );
                    return results;
                }

                for ( y = 7; y <= strlen(word); y++ )
                {
                    if ( word[y] == ')' )
                        break;

                    if ( word[y] == '\0' )
                    {
                        progbug( "Illegal use of GETVAR function (End of String Reached Before Finding a Closing Parenthesis)", mob );
                        return results;
                    }
                    add_letter( arg, word[y] );
                }

                for ( var = chkchar->first_var; var; var = var->next )
                {
                    if ( !str_cmp( var->name, arg ) )
                    {
                        strcat( results, format( "%s", var->value ) );
                        found = TRUE;
                    }
                }

                if ( !found )
                {
                    progbug( format( "Getvar: Var not found on char(%s)", arg ), mob );
                    strcat( results, "NullVar" );
                }
             }
         }
         if (cmnd[x] == '\0')
          return results;
         else
          add_letter( results, cmnd[x]);
      }
    }
  }
  return results;
}
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


93,926 views.

This is page 1, subject is 3 pages long: 1 2  3  [Next page]

It is now over 60 days since the last post. This thread is closed.     Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.