Discussion Total JassHelper overhaul project

Should JassHelper be updated?

  • Yes

    Votes: 15 60.0%
  • No

    Votes: 6 24.0%
  • Wait for Vexorian

    Votes: 4 16.0%

  • Total voters
    25

Bribe

vJass errors are legion
I'm going to be writing an entirely new compiler. It will be like Jass Shop Pro where it doesn't interface directly with World Editor or Jass NewGen Pack, so it will work on Mac OS X systems as well as PC's. It will have its own GUI with syntax highlighting, very beginner-friendly syntax checking that displays proper error messages, and of course it compiles as well. In fact, you won't even have to have WarCraft III installed on the computer in order to open, edit and save a project.

It will work with a map that was already saved with World Editor or Jass NewGen Pack so backwards compatibility will already be there by default.

Unlike Jass Shop Pro, the war3map.j contents will be invisible while editing, so you have an interface more like the trigger editor where each library can have its own frame, making it so you don't have to throw all of the script together yourself.

Fraction module (nearly-useless in the JASS environment, I have it here to show off the syntax):
Code:
lib Fraction:
"""
    Fraction, based on the fraction module from Python, allows you to do math
    without floating points & automatically simplifies ((3, 12) becomes (1, 4)). 
"""
    
    float class: # adds its members to the float class to allow syntax like bj_DEGTORAD.round()
        public int round():
            return R2I(self + float.tertiaryOp(self > 0, 0.50, -0.50))
        
        public bool rational(int p=3):
            p = R2I(10 ** p)
            return self * p == R2I(self) * p
        
    
    int class:
        public int length():
            return I2S(self).length()
    
        # Greatest-common-denominator for a fraction to be simplified into.
        public int getGCD(int num, int den):
            while den > 0:
                int pnum = num
                num = den
                den = pnum % den
            return num
    
    
    class fraction:
        
        readonly int num, den
        
        # f.set(1, 2) sets the numerator & denominator to whatever you want.
        public:
            fraction set(int a, int b):
                int d = int.getGCD(a, b)
                if d != 0:
                    self.num = a / d
                    self.den = b / d
                else:
                    self.num = 0
                    self.den = 1
                return self
        
            static fraction create(int num, int den):
                return fraction.allocate().set(num, den)
            
            # Ease-of-use API:
            
            static fraction fromInt(int i):
                fraction self = fraction.allocate()
                self.num = i
                self.den = 1
                return self
            
            static fraction fromFloat(float f, int p):
                p = R2I(10 ** p)
                return fraction.create(R2I(f * p), p)
            
            float product():
                return num / I2R(den)
        
            fraction copy():
                return fraction.create(num, den)
            
            nothing display(float duration):
                print(time=duration, "Fraction(\$num, \$den)")
            
            # Math API
            
            fraction add(fraction f):
                return self.set(self.num * f.den + self.den * f.num, self.den * f.den)
        
            fraction sub(fraction f):
                return self.set(self.num * f.den - self.den * f.num, self.den * f.den)
        
            fraction mul(fraction f):
                return self.set(self.num * f.num, self.den * f.den)
        
            fraction div(fraction f):
                return self.set(self.num * f.den, self.den * f.num)
        
            # Comparison API
            
            bool eq(fraction f):
                return self.num == f.num and self.den == f.den
            
            bool gt(fraction f):
                return self.num * f.den > f.num * self.den
        
            bool lt(fraction f):
                return self.num * f.den < f.num * self.den
        
    
    internal:
        timer clock = CreateTimer()
        fraction list[]
        int size = 0
    
    # Returns a self-destroying fraction instance.
    public fraction Fraction(int num, int den):
        fraction self = fraction.create(num, den)
        list[size] = self
        size++
        clock.start(0, false, lambda nothing:
            int i = size - 1
            size = 0
            while i >= 0:
                list[i].destroy()
                i--
        )
        return self
As you can see based on this script, I am using Python-style comments and block comments for personality's sake. JASS comments and vJass block comments will not parse.

The basics (many more things I don't have time to explain):
  • Primary language will be Luck. vJass backwards-compatibility will be added after the initial release. Zinc will not be supported. For now I have no plans for a c-like build.
  • hooks restructured to allow hooking either before or after. I will include the option to fully overwrite the function as well, but the function can only be overwritten once (syntax error for duplicate overwrite attempts).
    Code:
    # Overwrite hook
    hook:
        nothing PauseUnit(unit u, bool flag):
            if u.shouldBePaused:
                u.pause(flag)
            else:
                debug print("TRIED TO PAUSE UNIT THAT SHOULD NOT BE PAUSED!")
        
    # Hook after
    hook.after:
        nothing DestroyGroup(group g):
            if g == ENUM_GROUP:
                print("GROUP UTILS ERROR: ATTEMPT TO DESTROY ENUM_GROUP")
                ENUM_GROUP = CreateGroup()
    
    # Hook before is the current incarnation of vJass hooks
    hook.before:
        bool UnitDamageTarget(unit u, unit t, float f, bool ranged, bool attack, attacktype a, damagetype d, weapontype w):
            u.scriptedAttack = true
    hook.before and hook.after will only use trigger evaluations if there is more than one thing hooking it, otherwise they will be treated as simple function calls.
  • Placing a "\$" token within a ""-type string will treat its following segment as an I2S or R2S or basic string with concatenation.
    Code:
    print("JASS max array size is: \$JASS_MAX_ARRAY_SIZE")
     
    # is the same as:
    BJDebugMsg("JASS max array size is: " + I2S(JASS_MAX_ARRAY_SIZE))
     
    # You can also do:
    print(JASS_MAX_ARRAY_SIZE.name) # This produces a string of the variable name itself.
     
    # It tries to be as intelligent as it can, taking into account "." extensions and function calls.
  • You can design a function to have optional parameters.
    JASS:
    public unit NewUnit(player p=Player(0), int id='hfoo', float x=0, float y=0, float face=0):
        return CreateUnit(p, id, x, y, face)

    The above can be written dynamically, with intuitive variations.
    JASS:
    unit u = NewUnit()
    unit u = NewUnit('ewsp')
    unit u = NewUnit(face=180) # Use this flavor to skip to a specific param.
  • Anonymous functions as so:
    Code:
    onInit:
        trigger t = trigger.create()
        t.addCondition(Filter(lambda bool:
            # return false is added automatically if there are no return statements found.
        ))
  • Textmacros totally redefined as procedures to give maximum functionality. You can basically define your own compile-time rules now.
    Code:
    # Old:
    //! textmacro macro
            call DoNothing()
    //! endtextmacro
    //! runtextmacro macro()
    
    # New:
    procedure process():
        write:
            DoNothing()
    luck.process()
    To make sure procedures their own namespace, use "luck." as the prefix.
  • Modules are deleted in favor of merging them into procedures.
    Code:
    procedure module():
        include:
            readonly static float x
            readonly static float y # Only this procedure can set these
        include:
            onInit:
                x = 10
                y = 10 # Works.
        write:
            onInit:
                x = 10 # This works too. You just can't set them outside of the procedure.
  • Procedures can have variables of their own and use "config" objects as "globals".
    Code:
    config GLOBAL = 0
     
    procedure process(name, type):
        GLOBAL++
        wrapper = ""
        if type.type == handle:
            wrapper = '.getId()'
        elif type.type != int:
            throw "'type' must be an int or a handle!"
        local i = 10
        while i > 0:
            write:
                int GetData{GLOBAL}{i}({type} myVar):
                    return data.get(myVar{wrapper})
            i--
  • As you can see, curly brackets are now used instead of "$" tokens.
  • Procedures can be nested and taking into account multi-layed writing:
    Code:
    procedure A():
        write:
            DoNothing()
    procedure B():
        write:
            luck.A()
    Works like this:
    "A" writes "DoNothing()", "B" writes what A writes, so "B" effectively writes "DoNothing()"
  • You also have the bookmark keyword to specify where to write to.
    Code:
    procedure duo():
        write(myBookmark):
            t.addAction(lambda nothing:
                DoNothing()
            )
     
    luck.duo()
     
    onInit:
        trigger t = trigger.create()
        bookmark myBookmark
        # 't' automatically sets to null at the end because t.type is a handle.
  • Strings can be indicated either by '' or "". Single and quadruple-length ''-type strings will be treated as int rawcodes if passed as an int value, otherwise they will be treated as a string. The advantage is ''-type strings are taken literally so they avoid the need to escape quotes or escape backslashes.

    Current: [ljass]string s = "Abilities\\Weapons\\FarseerMissile\\FarseerMissile.mdx"[/ljass]

    New: [ljass]string s = 'Abilities\Weapons\FarseerMissile\FarseerMissile.mdx'[/ljass]
    This also makes it so a straight copy-paste of the model path is now possible without having to change everything to double-backslash, making your script a lot faster to tinker around with.
  • Writing "static if" is going to throw a syntax error. Normal if's should intelligently sort out which if's will be static and which will be dynamic.
  • All functions, variables and methods are treated as "internal" by default. The "public" keyword needs to be injected to indicate the object can be accessed outside of the library, though "private" and "protected" will be available and the "internal" keyword can be injected for clarity if you wish.
  • Script optimization is done automatically. Functions, variables, etc. that are not referenced are deleted during compilation. Shortening names is done as well.
  • Array structs and normal structs are treated very similarly which allows you to have total control. Allocate and deallocate can be overwritten and, as mentioned earlier, things that are not referenced are destroyed during optimization. Another thing, "onDestroy" will not use a trigger evaluation unless the struct is also extended. This should make it easier for users who don't want to bother with both overriding "destroy" and calling "deallocate" from it, while still preserving speed.
  • I will not include support for custom-defined arrays. Use of the Dictionary library is more clear and much more dynamic. Struct arrays are capped at 8192, nothing more or less.
  • The new "throw" keyword allows you to define your own syntax errors.
    JASS:
    if myStruct.onDestroy.exists:
        self.onDestroy()
    else:
        throw 'Error: &quot;onDestroy&quot; not found!'
  • Notice self is used here. Taking a step back for objectivity, I thought if there was no such thing as vJass or Python and I had to choose between "this" and "self", I chose "self".
  • Readonly library variables
  • Top-sort for structs instead of letting methods get turned into trigger evaluations if they are located below a method calling them.
  • All initializers inside of a library will run before all initializers in a library that requires it. This eliminates the need to hack the initialization process by abusing module initializers, as well as restore struct & library initializers as useful things. The initialization process inside of each library will be "priority->struct->library". "onInit.priority" is essentially the same as a module init.
  • "xor" added.

Please post requests, comments and/or constructive criticism.

Alternate discussion: http://www.hiveworkshop.com/forums/showthread.php?t=193557/
 

tooltiperror

Super Moderator
Staff member
As you know, JASSHelper is written in Delphi (object oriented version of Pascal) and is a bitch to modify.

As I'm sure you've read many times, it uses Gold Parsing System. The source code of JASSHelper is pretty strange, and really just ... blah.

Though I'm not sure I support what you're doing. The best thing to do would be to rewrite the whole thing in a lower level language (C++ ideally) which would be around 10 times faster in compiling and processing.

Don't mess with encapsulation. Please.

Lambda is a bit awkward, I'd prefer this:
JASS:
function onInit takes nothing returns nothing
call TriggerAddCondition(t, Condition(lambda
     return GetSpellAbilityId() == 'A000'
endlambda))
endfunction


TBH I really just wish you'd make a new language. Seriously. A brand new Preprocessor with a python inspired interface like Boa.
 
I forward motion to rewrite jasshelper code. The code is just so dern awful >.<. There are tons of C libraries for parsing languages.

Also, I agree with the lambda tooltiptucker suggested. I was about to bring that up too until I saw his ;D.

Also, modules that take parameters, private/public textmacros, private/public has a . instead of a _, and private from the outside just would be impossible to access because you can't do double . ;D.

Also, there should be an optimizer put in (like cJASS) that can be enabled/disabled... really, this should include all of the features of vex's optimizer + widgetizer ;o. There should be 2 maps outputted, one that is the regular map and the other with the suffix _opt. (if the optimizer is enabled w/ more than JASS optimizations).

Next, you should be able to extend primitive types with a struct like a unit, which changes the type of thistype (like regular structs extend integer, a struct that extends unit would have a type of unit, so you'd pass in a unit as a pointer).

Also, hooks shouldn't be able to return (that's stupid). If you hook a thing multiples times and continue to return, how on earth? : P

This would be an infinite loop
JASS:

hook CreateItem takes integer id, real x, real y returns item
    local item i = CreateItem(id, x, y)
    call SetItemUserData(i, 100)
    return i
endhook


You should do like hook.before and hook.after.

hook.after would do the hook after the native is called and take the native args + its return
hook.before would do the hook before the native is called and just take the native args.

I think that's a much smarter syntax.

Inherits shouldn't be inside of the struct, it should really just be a part of extend...
[ljass]struct C extends A,B[/ljass]

And the extends structs should be able to be accessed via A. and B.

The struct's instance is based on the first struct extended. If no struct extended, it does its own instantiation. Any struct after the first struct has its instance stored into an array.

I really suggest doing this in c++ : o. It might be faster to rewrite the whole thing than modify it.

edit
another neat feature might be just allowing the use of the end keyword to end any given block since you already know what the next end should be. Rather than endif, it could just be end. Rather than endfunction, it could just be end.

edit
and rather than extending natives, you should support handle typecasting
integer -> handle
handle -> integer

For example, a unit indexer be like
JASS:

typecast unit to integer
    return GetUnitUserData(unit)
end
typecast integer to unit
    return GetUnitById(integer)
end

local integer i = CreateUnit(...)
local unit u = i


And, you should automatically null handles that haven't been nulled that should be nulled at the end of a function. Check for = null and check when the handle is set. The set to null shouldn't always be at the end of the function, it should be placed smartly. If someone nulls it at a bad spot, you can also let them know ;o.

Let people know about unreachable code and remove it when saving.

Removed inlined functions if they aren't used in conditions, execute func, or actions.

Add an inline keyword for forcefully inlining a function.

Fix private scope

Make textmacros able to run in textmacros

Fix lua processing (the double save nonsense with object generation)

Make it so users can just type # instead of //! i.

Make it so that there is a general exe for lua in case users need like both object merger capabilities and path capabilities.

Rather than textmacros, it'd be better to just make the whole preprocessor language lua using # symbol. From there, users could define functions and make use of like a write method that writes lines to the file where they are used. Way better and smarter than textmacros. You should also be able to do optional lua code (# optional).
 

emjlr3

Change can be a good thing
Staff member
so JH is open-source, aka editable w/o permission?
 

Bribe

vJass errors are legion
The way hooks are currently done in vJass is stupid Nestharus, I know exactly what I had in mind when I wrote that.

It would compile this:

JASS:

call CreateItem(itemid, 0, 0)


Into:

JASS:

call HookCreateItem(itemid, 0, 0)


And obviously skip processing the value within the hook itself. That would be really stupid if I kept it hooking the hooked value, and despite what some might say I am not that thick.
 

tooltiperror

Super Moderator
Staff member
Be wary of listening to Nest. Seriously, don't listen to him D:

No comments on rewriting the whole thing? Please do it, we're here for moral support.
 
You'll see what I mean when you actually code it -.-.... you can't have the CreateItem in a hook.

I'll give you the scenario that is going to make you realize this using your syntax-
JASS:

hook CreateItem takes integer id, real x, real y returns item
    local item i = CreateItem(id, x, y)
    call SetItemUserData(i, 100)
    return i
endhook
hook CreateItem takes integer id, real x, real y returns item
    local item i = CreateItem(id, x, y)
    call SetItemUserData(i, 100)
    return i
endhook


boom, exploded because you have multiple returns... a hook can't return a value... It should be
JASS:

hook.before CreateItem takes integer id, real x, real y, item i returns nothing
    call SetItemUserData(i, 100)
endhook
hook.before CreateItem takes integer id, real x, real y, item i returns nothing
    call SetItemUserData(i, 100)
endhook


Boom, no explosion.

And if you make it so that a native can only be hooked one time, then you're causing even more problems...

There are also other reasons why your scenario explodes besides attempting to return multiple values. Your scenario would also create 2 items whenever the thing was called, and if you properly hook native calls inside of hooked natives, then it'd create infinite items.

Also, you need to use trigger evaluations with the hooks... vexorian had it right when he did it like that...

It should be just like hooks current work with the function call + global sets + trigger evaluation + etc. Hooks that are hooked before the native call go above the native and hooks that are after go below the native.

In fact, then we run into current design flaws with how vex did it when dealing with the op limit...

edit
Also, you know what my advice is like Bribe... think back to DamageEvent... and king's item indexer... and purge's combat state and revive script... think of the before and after -.-.
 

Bribe

vJass errors are legion
so JH is open-source, aka editable w/o permission?
Vexorian posted the entire source code. For every component of JassHelper. It's as open-source as it gets.

Nestharus, while I support the idea of # as a preprocessor operator, if you wrote a module which processes the job of the optimizer and widgetizer and worked on bettering the LUA mechanics, I'll make sure to work with you on such a task. But there are areas I am more attracted to right now and I am trying to set realistic goals so that something actually gets done.

Edit:

>> And, you should automatically null handles that haven't been nulled that should be nulled at the end of a function. Check for = null and check when the handle is set.

While I can do this but I think it's going too far into doing the programming FOR the user. You should know what needs to be nulled.

>> As you know, JASSHelper is written in Delphi (object oriented version of Pascal) and is a bitch to modify.

Going over the source code I can easily see what's parsing what. What I really don't like his naming conventions, some of them are really terrible.

>> another neat feature might be just allowing the use of the end keyword to end any given block since you already know what the next end should be. Rather than endif, it could just be end. Rather than endfunction, it could just be end.

I've given thought to this and it would be doable except for it would look entirely different.

So what I think needs to happen is:

1. I re-write the parser in low-level language.
2. I write a new language (Python would be fun).

To help me on this, I need to know:

1. A good language parser in C++ (Nestharus you mentioned many, but I want this one to be good so you have any ideas?)
2. Syntax ideas! And language ideas. Just share your opinions.
 
here is a page
http://dinosaur.compilertools.net/

keep it as JASS syntax unless you are planning on adding language code blocks... if you are planning on language code blocks, I suggest C syntax ;D. It's used by the biggest international languages- c, c++, c#, java =), and is also used in a variety of other languages.
 

Lyerae

I keep popping up on this site from time to time.
It would be better to rewrite the whole thing in C / C++. It would be much faster, and more people could actively work on it (more people know C / C++ than Delphi).

Edit: @Nestharus,
I definitely do not recommend continuing with the current JASS-style syntax. A C-based language would be much better (or even something like Python, with it's indentation-style syntax).
 

kingkingyyk3

Visitor (Welcome to the Jungle, Baby!)
I'm glad to hear it. :)
As long as it does not break the old syntax, then it is acceptable for me.
 

PurgeandFire

zxcvmkgdfg
Those are really nice fixes. I like this idea a lot. :thup:

I like the hook.before and hook.after idea, as well as the [ljass]inline[/ljass] keyword. (rather than auto-inlining, because some people prefer shorter codes over marginal speed differences) However, for the hooks, I personally would love it if you could intercept the function, to possibly make it not even called at all. That would be awesome. :)

As king said, make sure you do not break the backwards compatibility. All the old stuff should still work, and this should serve mostly as an extension and bug-fixer, rather than reinventing everything. IMO, I prefer it as normal JASS-style syntax, because then more people would be inclined to actually use it. But if its syntax is good enough, I won't mind.

Also, change .execute and .evaluate so they don't make both a trigger condition AND a trigger action, only the one associated with the one you are using.
 

Bribe

vJass errors are legion
I will fix .evaluate and .execute to not auto-copy the function unless the parameters call for it. For example, if a function takes nothing returns nothing, and is called by .execute, no cloned text will appear. Likewise, if it takes nothing returns boolean, and is called by .evaluate, no cloned text either.

I plan to preserve vJass syntax (not remove anything) but Zinc syntax will probably end up being turned more like C-syntax. A Python-style language will eventually emerge.

What should each version be called?

vJass -> keep the name?
Zinc -> if I change the syntax, I probably have to change the name?
Python-Jass -> I can think of some flattering name like "Bribe" - anyone else for ideas?
 

Laiev

Hey Listen!!
IMO:

vJass -> Yes, if will be another language, the name of this should be leave how its
Zinc -> I don't know, Zinc is so "non-warcraft" to me..
Python-Jass -> If you want to name it like Python-Jass, please, do something like PJass (Already got something with this name, no?), bJass, or something like this, at least if the API would be like:

function .. takes ... returns ...
call ...
set ...
endfunction
 

Frozenhelfir

set Gwypaas = Guhveepaws
I've been out of the loop for a while, but what about the struct allocator? I've seen it criticized by many for being incredibly slow. Is there any reason why this isn't on the list? As for the BJDebug compiling into an optimized version, I disagree with that. I've always thought that debug should only be used for testing purposes, and not in an actual released or finalized map. I vote yes because I see no reason not to have a more focused person updating JassHelper. I think this would be a large benefit to the mapping community because more stuff can be implemented faster.
 

Bribe

vJass errors are legion
The struct allocator will be adjusted so that its safety checks will only operate when in debug-mode, eliminating the complaint.

There's also a "kthis" variable that generates itself in structs' onDestroy method that is going to say good-bye.

BJDebugMsg displays text 16 times when it's called (once for each player... including computer players???), which doesn't matter, but 60 seconds display is pretty short if you have to make observations on a large quantity of data.
 

tooltiperror

Super Moderator
Staff member
Bribe has an MSN? :)

Bribe, since my main programming experience is in Web Development, I'll be a valuable ally when you want to write documentation. And by that I mean I want to script the whole page, the highlighting, and write it when you're ready, if you accept.

Name your new ZINC "Gallium", as Gallium is the element after zinc.

The idea of a python-based language was first introduced by Vexorian as Boa. The idea behind Boa was the beautiful python syntax, but I want something different. This is T32 in my language, I call it .

Code:
http://pastebin.com/T18VvjFX
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • The Helper The Helper:
    that sucks
  • jonas jonas:
    @midnight8 how is it looking
  • midnight8 midnight8:
    meh, me, wife and her friend thaty traveled with us all have covid, seems to be winding down some for me at least. Felt like complete shit, been sleeping a lot, just bieng lazy as hell. I feel like that today has probably been like 11 or 12 days since exposure. Have not had any fever today, and have not taken any meds, so hopefully by monday or tuesday I will be good to go, sadly have passed it on to our teen son, I guess that was pretty much unavoidable
  • The Helper The Helper:
    hope you get to feeling better and get vaccinated my friend
  • midnight8 midnight8:
    I will at some point, but so many vaccinated people still getting it. One of the bands we watch in vegas, all 4 had been vaccinated and are now positive.
  • Ghan Ghan:
    Symptoms?
  • Ghan Ghan:
    I think the symptoms are typically less severe if previously vaccinated.
  • midnight8 midnight8:
    I have had all of the symptoms, taste is slightly starting to come back, but smell, no. Honestly, just been a little miserable, have never felt in any danger from it. Being trapped at home sucks. lol
  • midnight8 midnight8:
    meh, got a little fever again this morning, guess gonna be a few more days
  • The Helper The Helper:
    I went to Comicon this last weekend I hope I dont get it I feel fine and I am not vaccinated and did not wear a mask
  • The Helper The Helper:
    Comicon really was not packed though like it was in the past. I am not really worried though it was the most people I have been around in a year.
  • tom_mai78101 tom_mai78101:
    Still, getting vaccinated is a good idea. We're getting Delta variant spikes here in Boston.
  • The Helper The Helper:
    I am not against vaccination at all I just have a serious procrastination problem I plan on getting vaccinated soon
  • midnight8 midnight8:
    was kinda same with me, I was gonna do it, and life got in the way.
  • midnight8 midnight8:
    we wore mask in some places, but at 118 degrees outside, little rough. :)
  • The Helper The Helper:
    yeah i had another friend in Vegas talking about that heat damn
  • The Helper The Helper:
    Well I do not think I got Covid from the Comicon
    +1
  • tom_mai78101 tom_mai78101:
    Pushed out a new Pokemon Walking algorithm build. With a new system in place, I'll probably start tackling triggers and NPCs,
    +1
  • Varine Varine:
    Is it fucking hot everywhere?
  • Varine Varine:
    What's a pokemon walking algorithm?
  • jonas jonas:
    it's an engine for pokemon games that closely emulates the walking behavior of pokemon red/blue/yellow
  • jonas jonas:
    basically if you wanted to implement pokemon yellow from scratch, and you'd want it to feel as close as possible to the real thing, you'd start with that
  • Varine Varine:
    Like from the Gameboy games? I'm not at all familiar with Pokemon.
  • jonas jonas:
    yeah
  • The Helper The Helper:
    I only played the Pokemon games on the Gamecube and Wii and such not on the portables my kids had all those games but I never really played on the portables. Now that I think about maybe once sooooo long ago.

    Staff online

    • Ghan
      Administrator - Servers are fun

    Members online

    Affiliates

    Hive Workshop NUON Dome
    Top