AT THE DROP OF A HAT I WILL THROW UP INTO A DROPPED HAT
barf
Patches accepted; Ports ported; segfaults fixed; much rejoicing
Things seem to be going pretty well! The PrBoom developers accepted that patch I made from Eternity's fix for the dropoff overflow. It's also gone into PrBoom-Plus! Yes, I am excited. Can I write "contributes to open source software" on my CV yet?
Speaking of PrBoom-Plus, apparently it builds on Linux now. I mean, I haven't tested this, and you could get it to work before by making a number of small changes to the code, but still. That was always one of my gripes about it. In fact the developers of both flavours of PrBoom are working together and seem to be on fire at the moment, there have been two (EDIT: three, hahaha) new versions released and more commits in the past month than the whole of 2005.
Other engines have recently added Linux support too. Chocolate Doom has always had it, Doom Legacy was even in Debian once until they realised it contained non-free code and took it out. Eternity appears to have gained an Autotools-based build system but requires a more recent version of SDL than is in Sarge. ZDoom has a Makefile.linux and has had some kind of support for some time - I managed to get it to build back in 2004. But it depends on non-free libraries so installing was a pain, and - while it ran okay - when I tried to leave, it crashed and took X along with it. But that's probably improved by now, you'd hope so anyway. Free software Doom is looking really strong right now.
But I'll probably stick with PrBoom for the moment, I am bloody-mindedly loyal like that. If you can call hacking on an abandoned branch of it loyalty, that is... But that's going pretty well too because this week I fixed the other two of the Three Great Annoying Crash Bugs That Have Plagued This Thing Since Forever (the Netcode Segfault, which is what I was talking about if you've ever heard me bitch about random segfaults, and a very rare crash when you exited the map which turned out to be savegame-related; the third was the dropoff overflow) Good news because they were all things I considered were release blockers.
Behaviour of GNU libc snprintf with write buffer in argument list
Unlike sprintf (which these days must always be referred to as "the unsafe sprintf") snprintf clears the write buffer before starting any processing. This has implications for calls that use the write buffer in the format arguments (I call this "snprintf with writeback" because you're trying to write back into one of the arguments) What's worse is that I can't find this behaviour documented anywhere.
Background
PrBoom 23x has its own snprintf implementation called psnprintf ("portable" snprintf, apparently) which it inherited from their ill-advised attempt to merge in much if not all of the SMMU/Eternity codebase. For rboom I decided to try to replace psnprintf with snprintf from the underlying libc. But guess what, everything broke.
The problem turned out to be this: In a number of places, 23x uses psnprintf to lazily append to a string that already exists, like so:
psnprintf(buffer, size, "%s%s", buffer, string);
To clarify, suppose you start with "abc" then append "def" using the above code; this works fine with psnprintf and you'd end up with "abcdef" in the buffer but with libc's snprintf you'd just get "def".
Now I know it's not very efficient but it does take care of null termination for you (it would seem that snprintf never writes more than size-1 characters and always puts a zero on the end). You could argue that you should be using strncat or something. But the size you pass to strncat is the maximum number of copyable characters, not the total size of the destination buffer, so you have to keep track of the length of the string already in the buffer which increases code complexity. Also you can only append character arrays; with snprintf you can append numbers, single characters, etc as well (and 23x does)
Workarounds
So far I have three workarounds that come with various tradeoffs of code complexity, efficiency, portability and so forth. Consider the following hypothetical join function that takes an array of strings and mashes them all together:
char *join(char *buf, size_t size, int argc, char **argv)
{
int i;
*buf = 0; // start off with an empty buffer
for (i = 0; i < argc; i++)
snprintf(buf, size, "%s%s", buf, argv[i]);
return buf;
}
Of course, the whole point is that with libc's snprintf, this won't work.
1. Stop being a pussy and keep track of the length like you know you should
This is probably what you should be doing, and, in this example, isn't very hard to add. The following example relies on snprintf returning the length of string it would have written if it hadn't run out of buffer. Some implementations don't do this, I guess you'd have to mess with strlen in that case.
char *join(char *buf, size_t size, int argc, char **argv)
{
int i, len;
for (i = 0, len = 0; i < argc && len < size; i++)
len += snprintf(buf+len, size-len, "%s", argv[i]);
return buf;
}
The format string and arguments no longer refer to the write buffer at all, so the problem doesn't occur. Note you no longer need to initialise the buffer, instead you have a variable len to keep track of how far through the buffer you are. You also break out of the loop early if the length written exceeds the size (it won't overflow the buffer of course, that's the point of snprintf, but snprintf is returning the number of characters it would have written)
So yes, as I say, this is probably the best method as it avoids having the write buffer in the argument list entirely. However, you have to do the length tracking at the same code level as the call to snprintf. You can't just write a wrapper around it that does this. That's okay here, in this example it's easy. However some of these calls in 23x are already inside fairly complex loops of their own. Adding in another variable to keep track of the written length is going to be extremely prone to errors, so I chickened out of it and decided to search for an easier solution.
2. Write to a different buffer and then copy it back
Obvious but inefficient solution. The following example exaggerates the inefficiency, and also uses C99 variable-length array declarations - if you can't do this, stunt something up with malloc (which will make it even more inefficient...)
char *join(char *buf, size_t size, int argc, char **argv)
{
int i;
char copy[size];
*buf = 0; // start off with an empty buffer
for (i = 0; i < argc; i++) {
snprintf(copy, size, "%s%s", buf, argv[i]);
memcpy(buf, copy, size);
}
return buf;
}
Here just for completeness is a version that uses malloc/realloc to keep a static buffer. Note the useful shortcut - calling realloc with a null pointer is equivalent to calling malloc. (You could also use asprintf, if available. I'll leave that as an exercise for the reader.)
char *join(char *buf, size_t size, int argc, char **argv)
{
static char *copy = NULL;
static size_t copysize = 0;
int i;
if (size > copysize)
copy = realloc(copy, (copysize = size));
*buf = '\0'; // start off with an empty buffer
for (i = 0; i < argc; i++) {
snprintf(copy, size, "%s%s", buf, argv[i]);
memcpy(buf, copy, size);
}
return buf;
}
Anyway, so basically, you have a second buffer you write into, to avoid writeback. Then you copy that into the original. So the string is going back and forth and back and forth... Obviously copying the whole buffer with memcpy is going to be a total waste of processing time; using strncpy to only copy up to the first zero byte would improve that. Furthermore you could break out of the loop early if you detect that snprintf has tried to write more bytes than the buffer size:
int len = snprintf(copy, size, "%s%s", buf, argv[i]);
strncpy(buf, copy, size);
if (len >= size) break;
But then you're practically doing length tracking anyway.
3. GROSS HACK ALERT
So after all this copying, I poked around a bit and it turns out that snprintf doesn't zero the whole buffer before doing any processing. In fact, it only zeroes the first element. So why not just preserve that one character?
char *join(char *buf, size_t size, int argc, char **argv)
{
int i;
if (argc == 0) {
*buf = '\0'; // if no arguments, return an empty buffer
} else {
snprintf(buf, size, "%s", argv[0]);
for (i = 1; i < argc; i++)
snprintf(buf, size, "%c%s%s", *buf, buf+1, argv[i]);
}
return buf;
}
This makes the huge assumption that it is only the first character that gets zeroed, but who's to say? I don't like this, it's clearly a hack.
Conclusion
- Length tracking is clearly the "best" way to do it but the hardest to write. You can't write an "append_snprintf" wrapper that appends to the buffer instead of overwriting it, and then simply replace the call to snprintf. You need to keep some state information - the current written length - between calls. You could do this by passing pointers to the buffer pointer and length and letting it update those, but they'd still need to be initialised. It's all extra complexity.
- A wrapper that allocates a second scratch buffer and writes into that is inefficient, but is it too inefficient? Perhaps not, but I don't like the argument that code only has to be "good enough" and "these days everybody has computers whose speeds and memory sizes involve the prefix 'giga' so who cares if you waste a few cycles" and "buy new hardware if the software runs too slowly". That shit is for idiot corporations who pay bad programmers far too high a salary.
- Making assumptions about the internal workings of library functions only leads to portability headaches. Not that I care too much about portability but I suppose it's the principle of the thing. I'd feel obliged to write complicated tests in the configure scripts and autoconf isn't exactly my strong point...
As for the apparent lack of documentation, well. At least in Debian Sarge it doesn't say anything about this on snprintf's manual page, or even in the libc info documentation. However I did some more searches and it turns out that the C99 standard itself says that "If copying takes place between objects that overlap, the behavior is undefined." That basically means, "don't do it". That's another reason to rule out the gross hack.
I think I'll just keep psnprintf for now, and maybe find or write a general string appending function later. Anyway, I hope someone found this interesting because once I got into it I quite enjoyed writing it!!
Selfish Episode
Your entire life is one long selfish episode, you introverted wanker. Ahaha oh burn, I insulted myself. No this is about Self-ep, a 7-level pwad for Doom 2. It's yet another project with Paul Corfiatis's name on it. In fact I believe he did the whole thing.
Back in the day... ah, yes, back in the day... I start looking up at the sky, everything goes misty and wavy... No, okay, look, all I was going to say was that I remember the first three Selfish maps when they first came out, in 2000 or thereabouts. Then self-ep was released, containing Selfish 1, 2 and 3. In fact it replaced them in /idgames, and they were deleted from it, so I can't link to the original versions. Then over the next few years, Selfish 4, Selfish 5, and Selfish X-Former came out. Logically those links shouldn't work, since they were replaced by the new self-ep.wad. But who cares about consistency... Indeed, Doomworld's /idgames frontend doesn't seem to update itself if a wad is rereleased and the text file changes, so if you click the link above it will still refer to the original three-map self-ep.wad release. Okay, got that? No misty eyes (just utter confusion, I imagine)
Anyway. The theme is roughly Doom 2 hell, although it varies. Decent architecture and texturing, it's not horribly overdetailed but not underdetailed either. Difficulty varies; map01 is trivial, map04 quite the opposite, the others somewhere in between.
- (Selfish X-Former) So you're in a brick building in Hell. Torches, crosses and squares of skin inscribed with pentagrams on the walls, that sort of thing. There's a red key to get, to open the exit. You have to travel through time and space or some goofy thing to activate two things to get that key. That is to say, there's two big teleporters that take you to an Episode 1-themed area and an Episode 2-themed area. It's all pretty nice looking, although a bit silly. As I said already, it's very easy too. There's crates in the E1 bit which will annoy purists and make people overly accustomed to climbing on crates to find secrets roll their eyes.
- (Selfish 1) Ah yes, the first Selfish map, this takes me back. You are in a cave which rapidly emerges out into the open air. You're out in a big canyon full of lava - don't fall in. Down a slope there's a building, but ignore it for now, the door needs a blue key. Furthermore there's lots of monsters around and you don't have the resources to take them out so try to find the opening in the rockface. In there is a lift. You should sprint to the lift and press the button before you get too damaged. Make your way around the caves. You should find a red key. Then there's a round staircase up to a blue key in the middle of a pit. You press two switches, a red and a yellow, to get to the switch that raises the bridge to the blue key. Don't make the mistake of looking for a yellow key, you don't need one. Poor use of textures! Anyway once you have the blue key and lots of ammunition you can go back up top and clear out the canyon, then go into the building, where you find the exit at the end of a corridor lined with cages full of monsters.
- (Selfish 2) The second Selfish map came out soon after the first one. I think it's marginally easier as you can clear it out "conventionally" (each room as you come to it) rather than having to run at the start and leave huge areas for later. The theme varies quite a lot, there's caves and wood panels and tech all in the same map. Pretty oldschool, I guess. The corridors with monster cages on either side make a return. There's a hot rocks and lava cave, and then the exit with a cyberdemon in a pit. It's an easy kill though if you find the invulnerability hidden in a pillar.
- (Selfish 4) This one was new to me. It's roughly in the same style as the connecting area of X-Former, but also has tech and castle bits. Doom 2 hell theme, see? It never was very consistent. Anyway, this one is noticably harder than the others, especially in regard to teleporting archviles and lack of ammunition. The defining moment was when about fifteen demons and an archvile teleport in, and you can't kill the archvile because the demons come in first and are in the way. So you try to shoot them, and it brings them back to life, then you run out of plasma. This is the only one I didn't manage to beat from a pistol start.
- (Selfish 5) Also new to me. Could be the lost level from Death Tormention 3. Is very much in an Episode 4 style, marble, blood etc. Quite large, lots of traps and stuff. Not too hard. One bit that annoyed me was the megasphere secret which only opens once. I don't like stuff that only works once. But otherwise it's a very nicely detailed E4-style map.
- (Selfish 3) Probably the biggest map here. In the style of Selfish 1, more than anything - large canyon, a few buildings dotted around the edge of it. The reason I keep on about Doom 2 hell is that the original Selfish 1, 2 and 3 replaced maps 21, 22 and 23, so they had that sky as well. Inside the buildings: one is a library, with an outdoor bit at the back. Watch out for teleporting archviles here - although one of them doesn't work, as extra lighting sectors have been added since the initial release and the tag is now on the wrong sector! The second is merely a corridor and lift taking you to the blue key. Watch out for pain elementals when you collect it. Yeah, pain elementals in a huge outdoor area where they've plenty of room to spawn. Oh dear. In fact one of my most frustrating deaths on this map was, in an effort to hide from the pain elementals so they wouldn't shoot until I could destroy them, I jumped from the blue key pillar, but missed the ledge and fell into one of the pits and died. Fortunately in this new version of the map there are teleporters in the pits. Anyway lastly you trek through a cave, past the exit, and into a little underground base thing where the last key is stored. The base seems a little out of place given the rest of the level's theme, but there you go. Very good.
- (Boss Battle) This is basically just a castle with a monster spawner in it. It was apparently made a very long time ago (1996) and tarted up for release. It's not bad, you don't have to mess around with shooting rockets off a moving platform or anything. You just have to go up a platform and shoot the brain once with your shotgun to open a door, then go press a switch which crushes it. There's a cyberdemon wandering around and of course monsters are being spawned but I never feel I have to kill the monsters on a spawner level.
So if you got through all that, the bottom line is, it's a decent wad that's worth playing. Off you go!
Efforts spent elsewhere
Few reviews recently as I have been doing more hacking on the game engine than actual playing. I don't really see the point of talking about that here though. As rboom isn't available anywhere, banging on about this bug fix or that feature isn't really giving out useful information, it's just going "look at me, look at all the cool things I've done! Wheee, no hands"
Having said that I did post some patches the other week, which you might find remarkably out of character. Now I'm caught by Warnock's Dilemma again!
Ogre Labs revisited
Indeed speaking of being shafted by Warnock's Dilemma and my own paranoia, I noticed with some satisfaction that Ogre Labs has been rereleased, with the bug fixes that were reported in the original thread. Of course, only a couple of weeks ago, I was complaining in some other thread that my carefully-written bug report on the pitfalls of using ZDoom for testing classic Doom maps had been ignored. Anyway, go download Ogre Labs and play it right now, it's excellent.
Grenian Doom
Grenian Doom is an 18-map PWAD released initially in 1998 and rereleased in 2001, which was when I became aware of it. I recently redownloaded it because I was thinking about two-sided linedefs that don't have the two-sided (transparent) bit set, but that's another story.
It's pretty oldschool. You wouldn't have been surprised if this had come out in 1995. Some of the maps are quite primitive, especially with texture usage. However, there's some good architecture and layouts. For instance, map01 (which I have just learned is also called Yaotzin) is one of those maps that... I don't know, it just has that spark that makes it really enticing, I don't know, it's hard to explain, it's just one of those maps. Use of height, a complex route, glimpses of places you can't get to, but then you find you can, etc. It's nonlinear, but there's definitely a "best" route to take, and finding it was very satisfying. Map02 has a similarly quite complex route but uses too much STARTAN3 in one place. Map06 was enjoyable too (and was the map I was thinking of, with the non-2s walls) and map15 is immense. On the other hand map04 is mostly a featureless, flat maze and is quite bland. Map07 is also a maze but an E2M2 crate maze, and map09 is a huge fortress with very high walls, and a spiderdemon in the middle. You have to do a lot of running, then teleport to the middle and hopefully snag the BFG before you get perforated.
Grenian is a mixed bag, quite literally. There's a certain feel to some of the levels that they've been converted from Doom to Doom 2 - lack of Doom 2 monsters and so forth. Map11 in particular appears to have been designed as an E2M8 replacement, but has had a normal exit added to replace the exit on cyberdemon death effect. I think the author has simply collected together all of his old maps and released them in a compilation wad. Fair enough.
So it's not going to impress anyone who doesn't like old school maps already, but it's not awful and I did like map01.
PS I almost forgot, it has a few sound replacements as well, the door open and close are okay but the pistol replacement is rubbish and I suggest you delete it with a wad editor or something
/idgames is playing up
For some reason trying to load any page on Doomworld, including its /idgames frontend results in an attempt to connect to the IP 208.184.36.73. Presumably this is an advert server. The problem is, the requests don't get a response and it takes a minute for it to time out. Unfortunately until this request does time out Firefox refuses to render the page. I don't know if this problem is my firefox configuration or NTL's webcaches or something else. So, I apologise if any of the links in the following sections appear to fail to load. Give it time or use w3m. Fortunately the forums still render promptly.
4 levels that got lost
The pcorf community project that ran on Doomworld for a while, until he got bored of it and released the 4 maps that were done. It starts with MAP05 for some reason.
- A tiny introduction, two brick/tech buildings connected by a garden. It is a bit like map01 of Scythe, but is very easy, I just did a 100% run in 46 seconds and I wasn't really trying.
- Smallish techbase map centred around a pool of nukage. Lots of detail, perhaps even too much. Lacks enjoyment due to being devoid of health and ammunition. I had to play it so carefully it was frustrating.
- This one made me laugh. Your start position is underneath a solid hanging body. The result being, you can't move, only spin round on the spot. Obviously it was only tested in source ports with proper thing height clipping. So I only saw one room, which was brick, and had four wooden doors leading away.
- Easily the best map of the four and worth the download. It's a complex well-interconnected base with just enough detail to look nice. Not too hard, except for being slightly top-heavy (the start is tricky with low ammo/weapons) Couple of well-hidden secrets. Oddly all the things in the map have bad flags, but PrBoom seemed to manage to correct them.
Binary ripple counter
"This is a 4-bit binary ripple counter implemented using voodoo dolls and conveyor belts. A simple example of how logic circuits can be constructed inside Doom levels using the Boom extensions."
Basically you've got these four squares with conveyor floors, with voodoo dolls on them. You press the button and some doors open and the dolls get pushed round the floors, tripping various linedefs as they go. This causes a binary counter on the wall to count up from 0000 to 1111.
It's very clever and if you're a big nerd you'll probably find it interesting. I did. I would never have thought of doing this in a million years. Especially since, having not been trained as a computer scientist, I have never heard the term "ripple counter"!
Europa Series
Some more of Erik Alm's earlier released maps. The first two share a common theme of a small room with six teleporters at the start. You pick one, and thus can start the map in six different places. Otherwise then you've got a very large, nonlinear map to explore. It will be stuffed with monsters and be very hard.
Eaeuro01 is just a Doom 2 styled map. There are lots of different themes used, lakes of nukage and lava, factory/industrial areas, and so forth. There are nine keys, three of each colour. You have to get one of each, but if you get all nine you get a bunch of secrets in the exit room, which is a small library in which a cyberdemon chases you. You don't really need the secrets though as its quite easy to kill it using the cover and the super shotgun. I don't think some of the keys are accessible to normal players anyway, one of the yellow ones is on a lift and requires a very tricky strafejump and you only get one chance as the switch doesn't work twice.
Eaeuro02 Same thing, only it looks better, uses the Gothic Textures, and plays better too. Only six keys this time, but uses Boom linedefs so they're all different. This is a large complex level and you may very well get lost. Once you know it, though, it's great. It's not very fun for 100% kills due to numbers of cacodemons in the large sea outside the map, but I found speedrunning on skill 2 to be good fun. Two of the secrets require archvile jumps or at least extremely tricky strafejumps that I managed once out of about 200 attempts. You can jump into the aforementioned sea from one point but it's no use as you can't get back out. Aside from a few little niggles this was a fine map to run around in and in my opinion is probably the best one.
There is also an Eaeuro03 which is based on the style of Vrack but I didn't play it. It often slows to a crawl due to detail. I might come back to it later.