Jump to content
The Dark Mod Forums

Understanding the Code


Recommended Posts

Do you think Gildoran would know how quotes are created in xdata definitions? He wrote the wiki article about the fileformat...

You can use the escaped double-quote for that \". It is important that you put two quotes \" \" in the same xdata line, as there is a problem with D3's idLexer class. I think this is a bug, but I'm not sure. The idLexer has problems when encountering a single escaped double-quote \" in a line, according to my experiments. I traced through the code but I didn't investigate further than that.

 

This, for instance, works (two quotes):

"page1_body" : "It has come to our underst\"a\"nding that guards took " 

This does not (only one quote):

"page1_body" : "It has come to our underst\"anding that guards took " 

 

The importer uses the DefTokeniser now and it works really good, despite the support for the import-directive. I'll have to do some investigation first to understand how exactly this import-directive works. Does it operate with multiple files or does it just use definitions of the same file?

You can have a look at the xdata parser in our sources darkmod/declxdata.cpp. The "import" directive can be used to import from any xdata declaration, from what I've read in the code. It's using a "lookup" call

declManager->FindType( DECL_XDATA, <import value here>, false );

The D3 parser is doing "lazy parsing" and parses the xdata blocks just before they're actually used. I tried to imitate that behaviour for the shader library when I wrote the DefBlockTokeniser.

 

Anyway, since you're working on the FontLoader now, I've got a feature request for you: A boolean method with a char/int parameter, telling the caller whether the requested glyph is available or not. The q3-font-array is an ASCII-table as far as I know, so requesting a certain character will be very comfortable. I'd like to use this method for informing the user when he typed a not-available glyph in the readable-Editor-Gui.

From what I've seen any character between 0-255 is valid and has a glyph information stored in the DAT file. If a character isn't available, it's just rendered as empty, I think (with its UV coordinates set to 0,0 in the texture).

 

I guess we can remove my FontLoader from the SVN then, right? I think all features have been integrated into your improved one...

Yes, I think it can be removed. If I need anything from it I can look it up in the SVN history.

Link to comment
Share on other sites

  • Replies 234
  • Created
  • Last Reply

Top Posters In This Topic

Top Posters In This Topic

Posted Images

This, for instance, works (two quotes):

"page1_body" : "It has come to our underst\"a\"nding that guards took " 

This does not (only one quote):

"page1_body" : "It has come to our underst\"anding that guards took " 

Yeah, I tried that one, but it didn't show up. Might have been an issue with the used font though. Ok then, one double-quote-support coming up... :)

 

The "import" directive can be used to import from any xdata declaration, from what I've read in the code.

Hmn, that sucks. I would have to read every xd file in order to get the required definition. We'd have to buffer all definitions and stuff like that beforehand. I won't add support for that just yet. Let's first see how the implementation looks like in the end and build up on that. Currently the user receives an errormessage saying that an import-directive was found and the respective definition is being discarded.

 

From what I've seen any character between 0-255 is valid and has a glyph information stored in the DAT file. If a character isn't available, it's just rendered as empty, I think (with its UV coordinates set to 0,0 in the texture).

Yeah, I think so too. I just thought it would be neat to have a method return it directly, e.g.:

bool Glyphset::checkGlyph(char n) { return (glyphs[n]->s != 0 || glyphs[n]->t != 0) }

Link to comment
Share on other sites

I'm going to start working on the GUI renderer, so I'll probably check in a few files in the dm.gui project. Just so that you're aware, it's best to regularly check in your code and keep your working copy up to date, to avoid running into conflicts. I'll try to keep the interaction zone very small so that I don't interfere with your work.

 

Also, be sure to subscribe to the darkradiant-svn mailing list, so that I can receive email notifications about your commits.

Link to comment
Share on other sites

I'm going to start working on the GUI renderer, so I'll probably check in a few files in the dm.gui project. Just so that you're aware, it's best to regularly check in your code and keep your working copy up to date, to avoid running into conflicts. I'll try to keep the interaction zone very small so that I don't interfere with your work.

Hehe, I guess I won't need to learn Open GL now then, after all!! ;) I'll concentrate on the simple tasks then, like gui-import and xdata-import. If you're doing the gui-renderer now it would probably be good if you defined the methods you need in "GuiLoader.h", which I planned to do the following tasks:

  • Parse the gui-File.
  • Load the backgroundtextures for the gui.
  • Load the fonts via your fonts-module.

We could of course also just make it a gui-parser and have the gui-renderer do all the dirty work.

 

Also, be sure to subscribe to the darkradiant-svn mailing list, so that I can receive email notifications about your commits.

I wasn't going to subscribe to the mailing list because on a huge project like this are so many commits and I didn't want to "spam" my mailfolder. =) But I'll just setup a filter then which moves the mails into a different folder. No problem!!

Link to comment
Share on other sites

I still get the email saying my post to the list needs to be approved by a moderator, although I am on the maillist now.

The reason it is being held:

Post by non-member to a members-only list

I guess you'll have to add me to the members list of that list in its admin area...

Link to comment
Share on other sites

I guess you'll have to add me to the members list of that list in its admin area...

I can't do that - orbweaver is the owner of that mailing list. Maybe the mailman stuff just needs a while for updating itself after your subscription, who knows. Not a big deal anyway.

Link to comment
Share on other sites

Are you developing in Win32 or x64? If you're on Win32, can you test whether a Win32 debug build successfully compiles (Rebuild Solution) on your end? I've made a lot of changes to the GTKGLext build rules, and this needs testing. The x64 builds are fine so far.

Link to comment
Share on other sites

I put you in the Developer role but I think this is just for "declarative" purposes. I also made you a "News Editor", whatever that is.

Didn't change anything. I guess you were right about Orbweaver having to make me member of the list.

Link to comment
Share on other sites

It's fixed... Thanks!

 

The XData class is as good as completed. The only major things left to do are:

  1. The support for the import-directive for whom I'll need a list of all xd-files in the VFS. I could acquire it myself with a FileVisitor or you hand if over to the XData-Class from the GUI, which would make sense, because you're probably going to make such a list anyway, aren't you?
  2. Migrating importXDataFromFile(..) from boost::filesystem to the VFS or make overloaded versions of both. I think the exporter should be kept in boost::filesystem, because we don't want to write into an archive, right? Could you tell me where to find a good example for using the VFS?

What do you think?

Link to comment
Share on other sites

The support for the import-directive for whom I'll need a list of all xd-files in the VFS. I could acquire it myself with a FileVisitor or you hand if over to the XData-Class from the GUI, which would make sense, because you're probably going to make such a list anyway, aren't you?

I haven't had a look at your importer since a few days, so I can't say how it's working currently. When traversing the .xd files, aren't you parsing all of them and adding those to a std::map? Or aren't you traversing anything at all right now? Examples for traversors can be found in the ParticlesManager class or the SoundFileLoader.

 

Migrating importXDataFromFile(..) from boost::filesystem to the VFS or make overloaded versions of both. I think the exporter should be kept in boost::filesystem, because we don't want to write into an archive, right? Could you tell me where to find a good example for using the VFS?

The exporter can be kept using boost::filesystem, yes. We don't want to alter any PK4s, DarkRadiant is designed to read from everything (PK4+OS), but should only ever write to the OS filesystem.

Link to comment
Share on other sites

Currently I am not traversing anything. The XData-class is just a basic-class for storing XData definitions, as well as managing/importing/exporting them, so up until now there was basically no need to for traversing files.

 

I'll do it within the class then...

Link to comment
Share on other sites

Ok, everything XData-related is done and working. I still have a couple of questions though:

  1. Can ScopedDebugTimer or GlobalFileSystem().forEachFile(...) throw exceptions that I would need to catch?
  2. I have integrated the operator() for the FileVisitor into my importer-class and I would like to hide it to public. Is there a way to befriend a class or function, so that I can make the typedef and the operator() private?
  3. For the import-directive to work properly, I have to search for every requested statement. Every search has to be started at the beginning of the XData-definition. Is there a better way to do it than reseting the get-pointer of the stream and requesting a new DefTokeniser in every Loop?

//Find the Definition 'SourceDef' in the File:
std::stringstream ss;
ss << &(file->getInputStream());
ss << ss.str().substr( ss.str().find(SourceDef) );
std::istream is(ss.rdbuf());
//Import-Loop: Find the SourceStatement in the file and pass the DefTokeniser to StoreContent(..)
for (int n = 0; n < SourceStatement.size(); n++)
{
is.seekg(0);
parser::BasicDefTokeniser<std::istream> ImpTok(is); //code-tag messes up the linebreaks here...
....
}

 

It would probably be good if you had a brief look at the headerfiles of XData and XDataLoader and possibly also their implementations, to check if you want anything changed.

 

If you have another coding task for me, bring it on! :)

Link to comment
Share on other sites

I've been loosely following your progress, but I haven't read through all of it to make suggestions. I noticed a couple of coding convention stuff, but I cannot comment on potential design issues yet.

 

Can ScopedDebugTimer or GlobalFileSystem().forEachFile(...) throw exceptions that I would need to catch?

No, I think these are safe, generally.

 

I have integrated the operator() for the FileVisitor into my importer-class and I would like to hide it to public. Is there a way to befriend a class or function, so that I can make the typedef and the operator() private?

There's no reason to have it private, in fact it wouldn't make sense, because the FileSystem wouldn't be able to use it as visitor. The operator() needs to be exposed, no way around that.

 

For the import-directive to work properly, I have to search for every requested statement. Every search has to be started at the beginning of the XData-definition. Is there a better way to do it than reseting the get-pointer of the stream and requesting a new DefTokeniser in every Loop?

The way we're doing it in the EntityClassManager is to parse all entityDefs first, and in the second run their inheritance is resolved. At that point all entityDef names are known. You can call that "deferred inheritance evaluation". One other way would be to parse only xdata names plus their raw blocks and then evaluate those blocks on demand, at which point the names are known as well.

 

Resetting stream pointers doesn't sound like clean design, and it wouldn't help in all cases. It's possible that xdata declarations are spread over multiple files, theoretically.

 

It would probably be good if you had a brief look at the headerfiles of XData and XDataLoader and possibly also their implementations, to check if you want anything changed.

Yeah, I was intending to give you feedback once you said you're ready, which seems to be the case now. It will take a while, I've been having a hard time finding time to code on my own stuff.

 

If you have another coding task for me, bring it on! :)

Hm. Apart from implementing feedback you'll be receiving from me, I can only think of other non-readables stuff.

 

My plan was to implement the GUI parser as next step (now that the CodeTokeniser is ready to be used, finally) - you can take over that task, if you like. I don't know how familiar you are with GUIs and their elements and declarations?

 

Next step is the GUI renderer, which is the hardest part, I guess. I don't want to hold you back, so if you're eager to take something specific on, let me know. You have much freetime ahead in the next few days?

Link to comment
Share on other sites

I'll be posting some stuff here, as I find it while reading your classes:

 

You're passing integers with ref-to-const:

 

virtual void resizeVectors(const int& TargetSize);

 

which is not really necessary. For primitive types like int, short, std::sizet, float, double, pointer* or enums they can just be passed by value. Some might say making them const is a good thing, but that depends on what the function is doing with them. Only for classes like std::string, shared_ptr, any STL container or other custom classes reference-to-const is the way to go, otherwise calls to copy-constructors will be issued (even if you don't specify copy-constructors yourself, the compiler will generate them silently for you, wherever needed and possible).

 

Also, the argument (and any local variables) should be lowercase. Only types (and some static methods) are uppercase in DarkRadiant. This is how I'd write the method above:

 

virtual void resizeVectors(int targetSize);

 

----

 

Minor: you're naming one enum "ContentChooser" - this is borderline matter of taste, of course, but I usually prefer type names to be as descriptive as possible. A "chooser" suggest that this is an actual class doing something choosing-related. In these cases, the enum is just used to specify which side the xdata is referring to, so you could just use "Side" as name for the enum. Or "ContentType" for Title & Body.

 

---

 

When appropriate you can specify method const-ness:

 

int getNumPages() const

{

return _numPages;

}

 

unless you're planning that your class is actually changing members when being called on that method. There are of course cases where method constness is just a pain in the ass but for such simple cases it doesn't hurt to add const.

 

---

 

Reading your comment here:

 

std::string getGuiPage(int Index) { return _guiPage[index]; } //can throw: vector subscript dimensions exceeded.

 

In release builds, you'll just receive a segfault when memory beyond the vector boundaries are accessed, in debug builds the best you can expect is an assertion being fired (which will just try to call your debugger), but there's no exception to be caught. I assume you're coming from C#? In .NET exceptions are much more common in these cases. Also, use std::size_t for accessing the vector, this has the advantage that no negative values can be passed to your method and you can assert index > 0. Plus, you can return a reference-to-const if you add const-ness to your method, so that the std::string doesn't have to copied to the caller.

 

So the function could look like that:

 

const std::string& getGuiPage(std::size_t index) const // also: lowercase argument name

{

if (index >= _guiPage.size()) throw std::runtime_error("GUI Page Index out of bounds.");

return _guiPage[index];

}

 

If you just want to catch such errors in debug builds, you can swap the if .. throw with an assert(index

 

---

 

There is this line in XData.h:181:

 

TwoSidedXData(std::string name) { _name=name; }

 

In this case, you should use initialiser lists to assign the value to _name:

 

TwoSidedXData(const std::string& name) :

_name(name)

{}

 

This prevents the std::string default constructor from being invoked right before "_name" is actually assigned the value of "name".

 

---

 

More to come. :)

Link to comment
Share on other sites

The way we're doing it in the EntityClassManager is to parse all entityDefs first, and in the second run their inheritance is resolved. At that point all entityDef names are known. You can call that "deferred inheritance evaluation". One other way would be to parse only xdata names plus their raw blocks and then evaluate those blocks on demand, at which point the names are known as well.

 

Resetting stream pointers doesn't sound like clean design, and it wouldn't help in all cases. It's possible that xdata declarations are spread over multiple files, theoretically.

The problem is that importable definitions aren't necessarily valid XData-readable-definitions (see Wiki). So importing a definition referenced by the import-directive may not work. Instead of importing, I open the file that contains the referenced definition, search for the requested statement and parse it into my XData-object. If there is more than one statement to parse, I reset the get-pointer to the beginning of that definition before every further search. Another possible approach would be to check if the current token is a requested statement, possibly parse the following content and repeat until the end of the definition is reached.

 

Currently, I just use the FileVisitor to create a DefinitionMap that stores all found definitionnames and the name of the file they are stored in. I use it to lookup, where to find a definition.

 

Hm. Apart from implementing feedback you'll be receiving from me, I can only think of other non-readables stuff.

 

My plan was to implement the GUI parser as next step (now that the CodeTokeniser is ready to be used, finally) - you can take over that task, if you like. I don't know how familiar you are with GUIs and their elements and declarations?

 

Next step is the GUI renderer, which is the hardest part, I guess. I don't want to hold you back, so if you're eager to take something specific on, let me know. You have much freetime ahead in the next few days?

There's not too much freetime the next days. I already spent way too much time today on finishing the first version of the XData-implementations, so I gotta concentrate on learning a little bit more, for now.

 

I am neither familiar with GUIs, nor with GL. But I would say, the GUI parser would be the easier task for me, because I already wrote a parser now and I'd basically only need to learn the GUI Syntax then. But I am still going to take a break for a couple of days. If there's still something to do afterwards, I'll do it. If not, also fine!

 

EDIT: Didn't see your post there. Thanks for the advice so far. I'll have a looksie at that later...

Link to comment
Share on other sites

Ok, I made every unsigned variable/argument size_t now, or should I rather use unsigned int for variables and size_t for arguments? I also now followed the proposed naming-conventions. I actually started with those conventions in mind, like they are shown in the DarkRadiant Coding-standards wiki, but somewhere along the line, I must have forgotten about them...

 

When appropriate you can specify method const-ness:

I should probably read the const-ness chapter in effective c++. I have never created a const method (and probably also never heard of it^^).

 

unless you're planning that your class is actually changing members when being called on that method. There are of course cases where method constness is just a pain in the ass but for such simple cases it doesn't hurt to add const.

What do you mean by that? (I really have to read the constness chapter) The numPages member is not const. No members are actually supposed to be const as I originally planned the class not only to be used for importing and exporting, but also for stuffing information from the readable editor GUI inside and passing the object to the preview renderer. But that's up to you now how you want to do that...

 

EDIT: Ah, ok. A const-method cannot change any members, but the members themselves can change as much as they want, right?

 

In release builds, you'll just receive a segfault when memory beyond the vector boundaries are accessed, in debug builds the best you can expect is an assertion being fired (which will just try to call your debugger), but there's no exception to be caught.

Ah I did not know those exceptions weren't generally available. Will add them manually then, because they are meant for communication with the Readable Editor GUI. But you did mean "index >= 0" with reference to std::size_t, right?

 

Although I did code a little C# (and really liked that language), I was originally taught C++ at the uni and Pascal and Logo at school (lol). I mostly worked with dialects of plain C however and assembly optimization, since what I do for a living is a lot closer to hardware, which is why I don't know all these neat tricks. If we want to be really precise, I actually started learning to code qBasic myself when I was pretty young... :)

 

TwoSidedXData(std::string name) { _name=name; }

 

In this case, you should use initialiser lists to assign the value to _name:

 

TwoSidedXData(const std::string& name) :

_name(name)

{}

OK, I forgot to use ref-to-const here, but when I try to assign name in an initializer list, my compiler returns errors: "illegal elementinitialization: '_name' is neither Basis, nor Element". I put it in those brackets and it worked. I actually don't quite understand this compiler-response, because the member-variables of the base-class are all protected and my class is a public inheritance of the base.

 

By the way, I will redesign the import-directive-support a little bit to allow multiple-stage import-directives. To achieve this, I'll use the other approach I suggested in the upper post.

 

Looking forward to your further comments. 'till then... B)

Edited by STiFU
Link to comment
Share on other sites

I also just made the resizeVectors() method a protected method, because I think this is nothing a client should need to worry about (yes, I already read the introduction of "Effective C++" :) ). The method is automatically invoked when setting numPages to a new value now and in the ctors of TwoSided and OneSided XData classes. The vectors are all initialized to MAX_PAGE_COUNT, which is currently 20.

 

The import() method generates a vector of error/warning messages and also adds a summary of the import process. Currently this vector is issued to cerr or globalOutputStream(), but I guess it'd be usefull to have that information available in the GUI later, so should I have the import return a pair<XDataPtrList, StringList> or maybe just use a reference argument for the ErrorList?

Link to comment
Share on other sites

One more thing: I just const-to-ref returned a local variable, hehe! But now at least I completely understand how this whole thing works. I guess to avoid the call of the copy constructor of my return variable, I'd have to pass the target-variable as a reference parameter right? This is probably essential for the import() method, which currently returns a whole vector of XData-objects.

Link to comment
Share on other sites

Ok, I made every unsigned variable/argument size_t now, or should I rather use unsigned int for variables and size_t for arguments?

It's not a matter of argument vs. variables. I generally prefer std::size_t over unsigned int whenever I need unsigned variables because std::size_t is not only unsigned (so it's able to do the same job as unsigned int), but it is also adjusting itself to the platform it's compiled on. I'm not 100% sure about unsigned int, but std::size_t is defined to be an unsigned integral type with the largest native bit width, i.e. a 64 bit integer in x64 environments and a 32-bit one in x86 builds. Although 4 billion is arguably enough for anything, I rather let the compiler choose. Plus, when dealing with STL containers (which are always using std::size_t as counting member) you can avoid warnings when comparing stuff to the return value of std::vector::size(). I fixed a bunch of x64 warnings in the past where code was written like this:

usigned int length = _vector.size();

In x86 builds, this is perfectly fine, but in x64 builds, the std::size_t is 64 bit is trimmed down to a 32 bit unsigned int (the VC++ compiler is emitting a warning about this). Using std::size_t in the first place is preventing that warning.

 

Not a big deal, admittedly, but I prefer it that way.

 

EDIT: Ah, ok. A const-method cannot change any members, but the members themselves can change as much as they want, right?

In the scope of a const-method no direct class members can be changed, unless they're declared mutable (which is to be avoided wherever possible, like reinterpret_cast). In const-methods you can't call non-const methods neither, nor can you return non-const-references to members (such that the caller might be able to change them using the non-const-reference).

 

But you did mean "index >= 0" with reference to std::size_t, right?

Yes, that's what I meant.

 

Although I did code a little C# (and really liked that language), I was originally taught C++ at the uni and Pascal and Logo at school (lol). I mostly worked with dialects of plain C however and assembly optimization, since what I do for a living is a lot closer to hardware, which is why I don't know all these neat tricks. If we want to be really precise, I actually started learning to code qBasic myself when I was pretty young...

Similar here, I had Logo on my Atari 800 XL, and later on I went into Pascal and Assembly. I didn't really know anything about C++ until I joined TDM and DarkRadiant. I had to learn C# along the lines when I picked up my current job. Plus a tiny bit of Perl and Python due to TDM.

 

OK, I forgot to use ref-to-const here, but when I try to assign name in an initializer list, my compiler returns errors: "illegal elementinitialization: '_name' is neither Basis, nor Element". I put it in those brackets and it worked. I actually don't quite understand this compiler-response, because the member-variables of the base-class are all protected and my class is a public inheritance of the base.

Ah, maybe the _name is belonging to the base class? That would make sense, because derived classes are not allowed to initialise base members in initialiser lists. In that case, the derived class would need to call the base class constructor which in turn is initialising its member, but whether this makes sense depends on the design. So my advice could well be inappropriate here.

 

The import() method generates a vector of error/warning messages and also adds a summary of the import process. Currently this vector is issued to cerr or globalOutputStream(), but I guess it'd be usefull to have that information available in the GUI later, so should I have the import return a pair or maybe just use a reference argument for the ErrorList?

I guess the XDataLoader could maintain that list, and hand it to the caller with an const ErrorList& XDataLoader::getErrorList() const method on demand.

Link to comment
Share on other sites

Ah, maybe the _name is belonging to the base class? That would make sense, because derived classes are not allowed to initialise base members in initialiser lists. In that case, the derived class would need to call the base class constructor which in turn is initialising its member, but whether this makes sense depends on the design.

Ah ok. The base-class is abstract, so using the initializer list is not an option here.

 

Did you miss my last post? I posted it while you were writing, so that's why I ask...

 

Anyway, thanks for teaching me so much. I have already learned a lot in the last weeks and my interest for coding has grown! :)

Link to comment
Share on other sites

One more thing: I just const-to-ref returned a local variable, hehe! But now at least I completely understand how this whole thing works. I guess to avoid the call of the copy constructor of my return variable, I'd have to pass the target-variable as a reference parameter right? This is probably essential for the import() method, which currently returns a whole vector of XData-objects.

Yes and sometimes no. When returning a std::string, in general the object is copy-constructed to the client (or assigned at the client, not sure).

 

In some cases though, the compiler's so-called "return value optimisation" is kicking in, where a returned object is not constructed, then returned and then assigned, but directly constructed at the client). This is heavily compiler-specific, and often compilers will refuse to do so. Your chances are high if "anonymous" objects are used to return stuff.

 

For instance:

const std::string FIRSTNAME("Joe"); // constant

std::string getFullName()
{
std::string returnvalue = FIRSTNAME;

returnValue += " ";
returnValue += "Mock";

return returnValue;
}

std::string result = getFullName();

Here, the compiler will have to copy-construct your return value.

 

However, you can increase the chance of return value optimisation by reorganising the code:

std::string getFullName()
{
return FIRSTNAME + " Mock";
}

Here there is no returnValue to be constructed and the compiler will probably write the returned result directly to the "result" string. Your mileage may vary depending on how aggressive the compiler is approaching this.

 

(I once read a book about this, so I might not have repeated everyting correctly, but you get the idea.)

 

This is a very special situation though and in many cases your methods will not be suitable for that optimisation. Generally (which is important to consider when doing that kind of "optimisation") you're highly unlikely to notice any difference in 97% of the cases, so pondering about these types of functions is wasted programmer time. The function is just not called often enough for a single copy constructor call to make any difference.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recent Status Updates

    • Petike the Taffer

      I've finally managed to log in to The Dark Mod Wiki. I'm back in the saddle and before the holidays start in full, I'll be adding a few new FM articles and doing other updates. Written in Stone is already done.
      · 4 replies
    • nbohr1more

      TDM 15th Anniversary Contest is now active! Please declare your participation: https://forums.thedarkmod.com/index.php?/topic/22413-the-dark-mod-15th-anniversary-contest-entry-thread/
       
      · 0 replies
    • JackFarmer

      @TheUnbeholden
      You cannot receive PMs. Could you please be so kind and check your mailbox if it is full (or maybe you switched off the function)?
      · 1 reply
    • OrbWeaver

      I like the new frob highlight but it would nice if it was less "flickery" while moving over objects (especially barred metal doors).
      · 4 replies
    • nbohr1more

      Please vote in the 15th Anniversary Contest Theme Poll
       
      · 0 replies
×
×
  • Create New...