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
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 ;)
#2
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 ;)
#3
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.

#4
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
#5
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("");
}
#6
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 JavaScript 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("");
}