mountain 3 crash fix mod (keeps the boulder spawners alive, one word of t1l5m003), object palette test skips without a display
This commit is contained in:
1 parent
ec2876d9c8
commit
526059c70a
7 files changed
+146
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
## 0.2.4 – unreleased
|
||||
|
||||
- **New mod "Mountain 3 crash fix"** (Gameplay): the rolling boulder spawners in Mountain chapter 3
|
||||
deleted themselves too early and their boulders later wrote into the freed memory (use after free),
|
||||
which can crash the game in `FreeLocalMem`. One word of level data keeps the spawners alive.
|
||||
- **New mod "Developer mode"** (Cheats): turns on the game's hidden developer switch. Hold A+S to
|
||||
warp to the developers' warp points, F1 to fly, F2 for a heart and 5 fruit, F3/F11 to count idols
|
||||
and coins up; Space on the river log makes it invincible and fast.
|
||||
|
||||
@@ -185,6 +185,12 @@ Plays every level mirrored left to right, like the mirror mode of racing games.
|
||||
| Readable textures | on / off | off | Flips writing and pictures back so they read normally. Some textures (water edges, paths) then show seams. Off: a true mirror image. |
|
||||
| Mirror object models | on / off | off | Also mirrors the shape of objects such as huts and crates (characters stay as they are). Off: objects are only placed mirrored. |
|
||||
|
||||
#### Mountain 3 crash fix
|
||||
|
||||
Fixes a bug in Mountain chapter 3 that can crash the game: the rolling boulder spawners delete themselves too early and their boulders later write into the freed memory. With the fix the spawners stay alive; the boulders roll as before.
|
||||
|
||||
*Id* `mountain3-fix`
|
||||
|
||||
#### Practice tools
|
||||
|
||||
Practise hard jumps: press F5 to remember where the llama stands and F8 to jump back there (in the same level; the keys can be changed below). Also adds extra checkpoints along the coin trail of levels that have checkpoints.
|
||||
|
||||
@@ -142,3 +142,9 @@ boot-to-level + mtn3-diag on the German exe). This is only for the German exe; n
|
||||
> without an error message, and "every so often" around boulders. The likely triggers are the
|
||||
> one-shot boulders near the start/top of the slope and the two red-X push boulders. We'll
|
||||
> probably ship the fix in LlamaMod.
|
||||
|
||||
## In LlamaMod
|
||||
|
||||
The fix is the mod `mountain3-fix` (`mods/fixes/Mountain3FixMod`, Gameplay): it finds the unique pattern in
|
||||
`t1l5m003.wad` and replaces `pushShort 0` with `pushShort 0.5`; if the pattern is missing (other game
|
||||
version, file already changed) it leaves the file unchanged and says so in the apply log.
|
||||
@@ -26,6 +26,7 @@ public final class ModRegistry {
|
||||
"com.eliasmueller.llamamod.mods.arena.ArenaMod",
|
||||
"com.eliasmueller.llamamod.mods.coaster.CoasterMod",
|
||||
"com.eliasmueller.llamamod.mods.devmode.DevModeMod",
|
||||
"com.eliasmueller.llamamod.mods.fixes.Mountain3FixMod",
|
||||
};
|
||||
|
||||
/** {@link ModSource}s loaded by class name (dynamic mods, e.g. level projects). */
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.eliasmueller.llamamod.mods.fixes;
|
||||
|
||||
import com.eliasmueller.llamamod.mods.LevelWads;
|
||||
import com.eliasmueller.llamamod.mods.Mod;
|
||||
import com.eliasmueller.llamamod.mods.ModContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Fixes the boulder use-after-free in Mountain 3 (t1l5m003): RollingBoulderMountain ends right after
|
||||
* spawning because it waits for its never-incremented counter v13 to be 0, while its boulders later
|
||||
* still write {@code parent.v13 -= 1} into the freed spawner. Comparing with 0.5 instead keeps the
|
||||
* spawner alive (frozen off camera). See notes/mountain3-crash.md.
|
||||
*/
|
||||
public final class Mountain3FixMod implements Mod {
|
||||
static final Pattern LEVEL = Pattern.compile("(?i)t1l5m003\\.wad");
|
||||
|
||||
/** freezeOffCamera 1; push v13; pushShort 0; eq; jzYield -4; end. */
|
||||
static final int[] PATTERN = {0x1002f, 0xd0004, 0x44, 0x4e, 0xfffc0012, 0xb4};
|
||||
|
||||
/** pushShort 0.5 (20.12) replacing pushShort 0 (third word of the pattern). */
|
||||
static final int FIXED = 0x08000044;
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return "mountain3-fix";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Mountain 3 crash fix";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Fixes a bug in Mountain chapter 3 that can crash the game: the rolling boulder spawners delete"
|
||||
+ " themselves too early and their boulders later write into the freed memory. With the fix the"
|
||||
+ " spawners stay alive; the boulders roll as before.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String category() {
|
||||
return GAMEPLAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(ModContext ctx) throws IOException {
|
||||
List<String> files = LevelWads.list(ctx, LEVEL);
|
||||
for (String f : files) {
|
||||
byte[] wad = ctx.read(f);
|
||||
int at = find(wad);
|
||||
if (at < 0) {
|
||||
ctx.progress(1, "Mountain 3 fix: pattern not found in " + LevelWads.fileName(f) + " – left unchanged");
|
||||
continue;
|
||||
}
|
||||
byte[] out = wad.clone();
|
||||
ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN).putInt(at + 8, FIXED);
|
||||
ctx.write(f, out);
|
||||
ctx.progress(1, "Mountain 3 fix applied");
|
||||
}
|
||||
}
|
||||
|
||||
/** Offset of the unique pattern, or -1 if it is missing or not unique. */
|
||||
static int find(byte[] wad) {
|
||||
ByteBuffer b = ByteBuffer.wrap(wad).order(ByteOrder.LITTLE_ENDIAN);
|
||||
int found = -1;
|
||||
outer:
|
||||
for (int i = 0; i + PATTERN.length * 4 <= wad.length; i++) {
|
||||
for (int k = 0; k < PATTERN.length; k++) {
|
||||
if (b.getInt(i + 4 * k) != PATTERN[k]) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
if (found >= 0) {
|
||||
return -1;
|
||||
}
|
||||
found = i;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ package com.eliasmueller.llamamod.editor.tools.objects;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeFalse;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.io.Bases;
|
||||
import com.eliasmueller.llamamod.editor.io.Project;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorPrefs;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
@@ -23,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
class ObjectPaletteTest {
|
||||
@Test
|
||||
void rightClickDoesNotPickTheType() throws Exception {
|
||||
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display (Swing popup)");
|
||||
Scene s = Project.create(Bases.fromCd(ObjectToolsTest.cd()), "new", "t1l1m001")
|
||||
.scene();
|
||||
List<String> tools = new ArrayList<>();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.eliasmueller.llamamod.mods.fixes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.eliasmueller.llamamod.mods.GameData;
|
||||
import com.eliasmueller.llamamod.mods.TestModContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class Mountain3FixModTest {
|
||||
@Test
|
||||
void changesExactlyOneWordInMountain3() throws IOException {
|
||||
Path cd = GameData.cd();
|
||||
Mountain3FixMod mod = new Mountain3FixMod();
|
||||
TestModContext ctx = new TestModContext(cd, mod, Map.of(), null);
|
||||
mod.apply(ctx);
|
||||
assertEquals(1, ctx.written.size(), "only t1l5m003 is written: " + ctx.written.keySet());
|
||||
String f = ctx.written.keySet().iterator().next();
|
||||
byte[] o = Files.readAllBytes(cd.resolve(f));
|
||||
byte[] n = ctx.written.get(f);
|
||||
int at = Mountain3FixMod.find(o);
|
||||
assertTrue(at >= 0);
|
||||
assertEquals(o.length, n.length);
|
||||
int diff = 0;
|
||||
for (int i = 0; i < o.length; i++) {
|
||||
if (o[i] != n[i]) {
|
||||
diff++;
|
||||
assertTrue(i >= at + 8 && i < at + 12, "change outside the fixed word at " + i);
|
||||
}
|
||||
}
|
||||
assertTrue(diff > 0 && diff <= 4);
|
||||
assertEquals(
|
||||
Mountain3FixMod.FIXED,
|
||||
ByteBuffer.wrap(n).order(ByteOrder.LITTLE_ENDIAN).getInt(at + 8));
|
||||
assertEquals(-1, Mountain3FixMod.find(n), "pattern is gone after the fix (applying twice is a no-op)");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user