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
|
## 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
|
- **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
|
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.
|
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. |
|
| 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. |
|
| 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
|
#### 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.
|
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
|
> 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
|
> 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.
|
> 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.arena.ArenaMod",
|
||||||
"com.eliasmueller.llamamod.mods.coaster.CoasterMod",
|
"com.eliasmueller.llamamod.mods.coaster.CoasterMod",
|
||||||
"com.eliasmueller.llamamod.mods.devmode.DevModeMod",
|
"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). */
|
/** {@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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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.Bases;
|
||||||
import com.eliasmueller.llamamod.editor.io.Project;
|
import com.eliasmueller.llamamod.editor.io.Project;
|
||||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||||
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
||||||
import com.eliasmueller.llamamod.editor.ui.EditorPrefs;
|
import com.eliasmueller.llamamod.editor.ui.EditorPrefs;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
import java.awt.event.InputEvent;
|
import java.awt.event.InputEvent;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
@@ -23,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
class ObjectPaletteTest {
|
class ObjectPaletteTest {
|
||||||
@Test
|
@Test
|
||||||
void rightClickDoesNotPickTheType() throws Exception {
|
void rightClickDoesNotPickTheType() throws Exception {
|
||||||
|
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display (Swing popup)");
|
||||||
Scene s = Project.create(Bases.fromCd(ObjectToolsTest.cd()), "new", "t1l1m001")
|
Scene s = Project.create(Bases.fromCd(ObjectToolsTest.cd()), "new", "t1l1m001")
|
||||||
.scene();
|
.scene();
|
||||||
List<String> tools = new ArrayList<>();
|
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