Name the Battle Tower's ZABOROWSKI instead of refusing the battle
BattleTowerTrainers' names are ten bytes padded with "@" only when shorter, and the cartridge copies the ten bytes to wOTPlayerName before adding the terminator. The port read the name as an "@"-terminated string, so drawing ZABOROWSKI (trainer 5) threw "Unterminated string." and the challenge was refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3be732118
commit
acdaf62eaa
3 files changed
+45
-3
No files matched your search
@@ -2,7 +2,7 @@ import { JOHTO, RAM, WRAM } from "../rom/profile";
|
||||
import { maxPP, validPartyMon, type PartyMon } from "../party/pokemon";
|
||||
import type { PokemonData } from "../rom/pokemon";
|
||||
import { DIRECTION_ORDER, type Direction } from "../world/types";
|
||||
import { readString } from "./text";
|
||||
import { readFixedString, readString } from "./text";
|
||||
import type { Gen, SpecialContext } from "./special-context";
|
||||
|
||||
/** BATTLETOWER_* (constants/battle_tower_constants.asm). */
|
||||
@@ -209,7 +209,9 @@ export function loadOpponent(c: SpecialContext, bt: BattleTowerState): BattleTow
|
||||
const trainer = sample(c, BATTLE_TOWER.uniqueTrainers, (index) => bt.trainers.includes(index));
|
||||
bt.trainers[bt.beaten] = trainer;
|
||||
const entry = JOHTO.BattleTowerTrainers + trainer * TRAINER_ENTRY;
|
||||
const name = readString(r, entry, c.text(), TRAINER_ENTRY - 1);
|
||||
// Copied to wOTPlayerName with a "@" after its ten bytes: ZABOROWSKI
|
||||
// fills them.
|
||||
const name = readFixedString(r, entry, c.text(), TRAINER_ENTRY - 1);
|
||||
const trainerClass = r.byte(entry + TRAINER_ENTRY - 1);
|
||||
// LoadRandomBattleTowerMon: wBT_OTMon1-3 start as species 0 and item $ff.
|
||||
const species = [0, 0, 0],
|
||||
|
||||
@@ -153,6 +153,25 @@ function character(code: number, context: TextContext): string | undefined {
|
||||
return CHARACTERS[code];
|
||||
}
|
||||
|
||||
/** Decode a name of at most `length` characters, as CopyBytes followed by
|
||||
* a "@" leaves it: one that fills its field has no terminator of its own. */
|
||||
export function readFixedString(
|
||||
reader: RomReader,
|
||||
start: number,
|
||||
context: TextContext,
|
||||
length: number,
|
||||
): string {
|
||||
let output = "";
|
||||
for (let p = start; p < start + length; p++) {
|
||||
const code = reader.byte(p);
|
||||
if (code === 0x50) break;
|
||||
const value = character(code, context);
|
||||
if (value === undefined) throw new UnsupportedText(p, code);
|
||||
output += value;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/** Decode a plain "@"-terminated string such as a name or item label. */
|
||||
export function readString(
|
||||
reader: RomReader,
|
||||
|
||||
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { GameWorld } from "../src/game/world/world";
|
||||
import { calcStats, generatePartyMon, maxPP, type PartyMon } from "../src/game/party/pokemon";
|
||||
import { CHALLENGE } from "../src/game/scripts/battle-tower";
|
||||
import { CHALLENGE, emptyBattleTowerState, loadOpponent } from "../src/game/scripts/battle-tower";
|
||||
import type { SpecialContext } from "../src/game/scripts/special-context";
|
||||
import { Playthrough } from "./playthrough-driver";
|
||||
|
||||
const romPath =
|
||||
@@ -88,6 +89,26 @@ describe.skipIf(!existsSync(romPath))("the Battle Tower with the local ROM", ()
|
||||
expect(p.world.notice, p.world.diagnostic).toBe("");
|
||||
};
|
||||
|
||||
it("names a trainer whose name fills all ten characters", () => {
|
||||
// BattleTowerTrainers' sixth entry, ZABOROWSKI, has no "@" of its own.
|
||||
let seed = 5;
|
||||
const context = {
|
||||
reader: assets!.reader,
|
||||
data: assets!.pokemon,
|
||||
random: () => {
|
||||
seed = (seed * 1103515245 + 12345) >>> 0;
|
||||
return (seed >>> 16) & 255;
|
||||
},
|
||||
text: () => ({ player: "KAWA", rival: "SILVER", mom: "MOM", strings: {}, weekday: "SUNDAY" }),
|
||||
} as unknown as SpecialContext;
|
||||
const names = new Map<number, string>();
|
||||
for (let draw = 0; draw < 300 && !names.has(5); draw++) {
|
||||
const opponent = loadOpponent(context, { ...emptyBattleTowerState(), levelGroup: 1 });
|
||||
names.set(opponent.trainer, opponent.name);
|
||||
}
|
||||
expect(names.get(5)).toBe("ZABOROWSKI");
|
||||
});
|
||||
|
||||
it("is reached from Route 40 through its gate", () => {
|
||||
const p = new Playthrough(assets!);
|
||||
p.world = new GameWorld(assets!, p.now, p.random);
|
||||
|
||||
Reference in new issue
Block a user