Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - DirtyHairy

#1
Menu screens are coming along, including a live preview of the next level. Sticking to original graphics doesn't mean that you can't put those extra pixels that you get on a modern display to work... ;)
#2
Other Projects / Re: Rustlings: a WIP lemmings engine
November 04, 2026, 10:32:43 PM
Alright, I have publised a 0.1.0 release with binaries for MacOS, Windows and Linux here: https://github.com/DirtyHairy/rustlings/releases/tag/0.1.0

Just dump the binary next to the DOS data files and launch it (or point it to the data directory from the command line). All levels should be fully playable, but there is not detection of end conditions yet, the level continues to run when the last lemming is gone (or time is up) until you switch to another level.

  • Page up / down to switch levels
  • Left / right to scroll, hold shift to scroll fast. In fullscreen you can also move the mouse to the edges to scroll.
  • p for pause
  • + / - to adjust release rate
  • 1 - 8 to switch skill
  • hold shift to emulate holding the right mouse button on MacOS
  • ctrl/cmd enter to toggle fullscreen
  • ctrl/cmd q to quit

In case you try it: have fun!
#3
Other Projects / Re: Rustlings: a WIP lemmings engine
November 03, 2026, 07:44:10 AM
Thanks a lot for the kind words!

Quote from: Simon on November 02, 2026, 09:06:31 PMAnd you've reverse-engineered parts of DOS L1 yourself. We believe that Lemmix and ccexplore's descriptions replicate DOS L1 exactly, but it's always possible that there are bugs that we haven't found even after 20 years of Lemmix.

To be fair, the parts that I reversed from the DOS binary so far are (from the top of my head) are the level code encoding, level ordering & oddtable, particles (I wanted to make sure that I am using the exact particle data and algorithm as the original), skill panel palette and a few more things that I probably forgot. My physics code currently is mostly a 1:1 port of the mechanics implemented by Lemmix. I did do a frame by frame comparison with the DOS binary though, so at least the aspiration is to be a pixel and bug perfect copy :P

Quote from: Simon on November 02, 2026, 09:06:31 PMStill, I'll see how much modern convenience, replay fine-control, ... you'll add.

I'll add the original menu screens (but with a live preview of the level) and the title screen next, and after that it's probably audio time. And when that's done... who knows.  :8:()[:

Quote from: Simon on November 02, 2026, 09:06:31 PMYou show VGA level graphics with the EGA skill panel and information bar. Is that intentional?

As The Tomato Watches says, this is actually how DOS looks in VGA in "High Performance" mode. Lemmings uses the same display code and 16 color mode for VGA as for EGA and just takes advantage of the larger color space (probably for code reuse, and faster blits). The difference between "High Performance PCs" and "PC compatibles" are that the latter uses the same palette for the skill panel as for the rest of the level, while the "high performance" mode does a palette switch just before it displays the panel and uses a different palette there.
#4
Other Projects / Re: Rustlings: a WIP lemmings engine
November 01, 2026, 09:11:39 PM
After picking up Rustlings again, all skills are now implemented. There still is no checking for game over conditions, and there still are no menus, but all levels are playable. I still want to add a few unit tests for builders, and then I'll put up test builds for MacOS, Linux and Windows that you can try out if you feel adventurous later this week  ;)
#5
Progressing towards "playable" ;) Walking, falling and all kinds of "death by terrain" are now working and match frame-to-frame with the original.
#6
Quote from: Mindless on March 22, 2026, 10:35:33 PMArrows can also be edited to be black (mimicking the Sega Genesis style of arrows).

According to https://www.camanis.net/lemmings/files/docs/lemmings_lvl_file_format.txt this changes the modifier to an invalid value... interesting. This makes me want to track down and disassemble the drawing code even more ;)
#7
Thanks for the info, then my memory is wrong. Nah, I am fine with keeping them blue, I am just curious. Actually, my hypothesis was that Lemmings DOS may be using VGA / EGA read mode 1 to mask out pixels that have black (= no terrain) below them, and that maybe dosbox may have an emulation bug there. But, obviously, I have not gone through the pain of tracking down and disassembling the corresponding code yet (although I have disassembled parts of the binary), and I figured that I'd just rather ask if anyone has more fresh data on how the DOS version actually looked than my 35 year old childhood memory ;)
#8
I am currently writing yet another replacement engine for DOS Lemmings. Comparing to dosbox I have noticed something that I didn't notice before: on most tilesets, the color of the arrows as my engine reads them from the original game assets and displays them differs from what is shown in dosbox. For example, on marble levels, the arrows should be blue according to the assets, but dosbox displays red arrows.

On the web, I find screenshots with both colors, but unfortunately no indicaion of the emulator used (and I cannot find images from a real VGA screen). I had a copy of Lemmings on our good, old 286 when I was a child, and I remember them being blue, but that's about ~35 years ago, so I wouldn't trust my memory too much here. I suspect a dosbox bug, but there is no way to tell for sure without looking at real hardware.

So, does anyone remember (or better yet: still run a real hardware setup) how those arrows looked on real hardware? There's two screenshots below, on from my engine, the other from real hardware.

#9
Other Projects / Rustlings: a WIP lemmings engine
March 15, 2026, 04:56:24 PM
I am currently working on a cross-platform reimplementation of DOS Lemmings in Rust that reads the original data files and acts as a drop in replacement for the original binary. I have yet to add the actual gameplay, but the interface and the basic engine are now in place and ready for some rodents to move in, so I thought I'd share a few screenshots ;) If you're curious you can find the project at https://github.com/DirtyHairy/rustlings
#10
Actually, turns reality is slightly more complicated ;) On my actual copy of lemmings, the code for the next level depends on the percentage of lemmings rescued when completing the current one, and all different codes generated this way work. In addition, there is a variable (that I called "salt" in the listing below), the lowest 4 bit of which are encoded in the code. All different salts generate valid codes, and restoring a code also restores the salt, so subsequent codes will encode the same value. Or, in other words, if you enter a code for a different percentage you'll get the same codes for the next levels, but if you enter a code with a different salt you'll get a different set of codes.

// rating:        0 .. 4
// level:         0 .. 29
// percentage:    0 .. 100
// salt:          0 .. 0x0f
function codeFor(rating, level, percentage = 100, salt = 0x00) {
  const secret = "AJHLDHBBCJ".split("").map((x) => x.charCodeAt(0));

  const levelIndex = 30 * rating + level;
  const code = new Array(10).fill(" ".charCodeAt(0));

  code[0] = ((levelIndex & 0x01) << 3) | secret[0] | ((salt & 0x01) << 1) | ((percentage & 0x01) << 2);
  code[1] = ((levelIndex & 0x02) << 1) | secret[1] | ((percentage & 0x02) >> 1);
  code[2] = (levelIndex & 0x04) | secret[2] | ((percentage & 0x04) >> 1) | ((salt & 0x02) >> 1);
  code[3] = ((levelIndex & 0x08) >> 3) | secret[3] | ((percentage & 0x08) >> 2);
  code[4] = ((levelIndex & 0x10) >> 3) | secret[4] | ((percentage & 0x10) >> 1) | ((salt & 0x04) >> 2);

  code[5] = ((levelIndex & 0x20) >> 5) | secret[5] | ((percentage & 0x20) >> 3) | ((salt & 0x08) >> 2);
  code[6] = ((levelIndex & 0xc0) >> 4) | secret[6] | ((percentage & 0x40) >> 6);
  code[7] = (levelIndex & 0xf) + secret[7];
  code[8] = (levelIndex >> 4) + secret[8];

  code[9] =
    ((code[0] + code[1] + code[2] + code[3] + code[4] + code[5] + code[6] + code[7] + code[8]) & 0xf) + secret[9];

  for (let i = 7 - (levelIndex & 0x07); i > -1; i--) {
    const tmp = code[6];
    code[6] = code[5];
    code[5] = code[4];
    code[4] = code[3];
    code[3] = code[2];
    code[2] = code[1];
    code[1] = code[0];
    code[0] = tmp;
  }

  return code.map((x) => String.fromCharCode(x)).join("");
}
#11
Howdy!

I few years ago I wrote a lemmings 1 data decoder and viewer ("rustlings") in an effort to get to know rust. At this point, it is capable to decode the original data files, extract level and graphics data and display sprites and levels. You can find the source here: https://github.com/DirtyHairy/rustlings

A few days ago I started working on it again, with the intend of expanding it into a full game engine. Since then I have spent a bit of time poking the original .exe with Ghidra to reverse a few bits and pieces that I am missing (or do not just want to read off from existing reimplementations), and on the way I also reversed the code generation algorithm. As I couldn't find much about this online, here's a javasΞript version that can generate the codes for all original levels, in case anyone else comes looking for the same thing:

// rating and level both zero based
function codeFor(rating, level) {
  const secret = "AJHLDHBBCJ".split("").map((x) => x.charCodeAt(0));
  const percentage = 100;

  const levelIndex = 30 * rating + level;
  const code = new Array(10).fill(" ".charCodeAt(0));

  code[0] = ((levelIndex & 0x01) << 3) | secret[0] | ((percentage & 0x01) << 2);
  code[1] = ((levelIndex & 0x02) << 1) | secret[1] | ((percentage & 0x02) >> 1);
  code[2] = (levelIndex & 0x04) | secret[2] | ((percentage & 0x04) >> 1);
  code[3] = ((levelIndex & 0x08) >> 3) | secret[3] | ((percentage & 0x08) >> 2);
  code[4] = ((levelIndex & 0x10) >> 3) | secret[4] | ((percentage & 0x10) >> 1);
  code[5] = ((levelIndex & 0x20) >> 5) | secret[5] | ((percentage & 0x20) >> 3);
  code[6] = ((levelIndex & 0xc0) >> 4) | secret[6] | ((percentage & 0x40) >> 6);
  code[7] = (levelIndex & 0xf) + secret[7];
  code[8] = (levelIndex >> 4) + secret[8];
  code[9] =
    ((code[0] + code[1] + code[2] + code[3] + code[4] + code[5] + code[6] + code[7] + code[8]) & 0xf) + secret[9];

  for (let i = 7 - (levelIndex & 0x07); i > -1; i--) {
    const tmp = code[6];
    code[6] = code[5];
    code[5] = code[4];
    code[4] = code[3];
    code[3] = code[2];
    code[2] = code[1];
    code[1] = code[0];
    code[0] = tmp;
  }

  return code.map((x) => String.fromCharCode(x)).join("");
}