1 import PropTypes from 'prop-types';
2 import React from 'react';
3 import { useTranslation } from 'react-i18next';
5 import Icon from '../common/Icon';
6 import ToggleSwitch from '../common/ToggleSwitch';
8 compareGTBasementState,
11 } from '../../helpers/alttp-ram';
12 import { useSNES } from '../../hooks/snes';
14 const IN_GAME_MODES = [
16 0x06, // entering dungeon
18 0x08, // entering overworld
20 0x0A, // entering special overworld
21 0x0B, // special overworld
22 0x0E, // text/menu/map
31 0x18, // aga 2 cutscene
32 0x19, // triforce room
40 0x04, // fast ganon (the default and used for defeat ganon for some reason)
41 0x07, // crystals & bosses
43 0x09, // all dungeons, no aga 1
44 0x0B, // completionist
47 const FREE_ITEM_MENU = 0x180045;
48 const GT_CRYSTALS = 0x18019A;
49 const GANON_TYPE = 0x1801A8;
50 const SEED_TYPE = 0x180210;
51 const INIT_SRAM = 0x183000;
53 const GAME_MODE = 0x10;
54 const CURRENT_DUNGEON = 0x10E;
55 const SAVE_WRAM = 0xF000;
56 const ROOM_DATA_START = 0x000;
57 const ROOM_DATA_END = 0x140;
58 const PYRAMID_SCREEN = 0x2DB;
59 const BIG_KEYS_1 = 0x366;
60 const OWNED_CRYSTALS = 0x37A;
62 const GT_ENTRANCE_ID = 55;
64 const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => {
65 const [enabled, setEnabled] = React.useState(false);
66 const controls = React.useRef({
72 const [inGame, setInGame] = React.useState(false);
73 const [seedType, setSeedType] = React.useState(0);
74 const [gtCrystals, setGTCrystals] = React.useState(0);
75 const [ganonType, setGanonType] = React.useState(0);
76 const [freeItemMenu, setFreeItemMenu] = React.useState(0);
77 const [pyramidOpen, setPyramidOpen] = React.useState(false);
79 const [ownedCrystals, setOwnedCrystals] = React.useState(0);
80 const [lastEntrance, setLastEntrance] = React.useState(0);
81 const [hasEntered, setHasEntered] = React.useState(false);
82 const [basement, setBasement] = React.useState({
83 state: getGTBasementState(),
88 const [hasBigKey, setHasBigKey] = React.useState(false);
96 const { t } = useTranslation();
98 React.useEffect(() => {
104 }, [onSolve, onStart, onStop]);
106 const resetState = React.useCallback(() => {
115 setHasEntered(false);
117 state: getGTBasementState(),
125 const enable = React.useCallback(() => {
130 const disable = React.useCallback(() => {
136 React.useEffect(() => {
137 const savedSettings = localStorage.getItem('guessingGame.settings');
139 const settings = JSON.parse(savedSettings);
140 if (settings.autoTrack) {
146 const saveSettings = React.useCallback((newSettings) => {
147 const savedSettings = localStorage.getItem('guessingGame.settings');
148 const settings = savedSettings
149 ? { ...JSON.parse(savedSettings), ...newSettings }
151 localStorage.setItem('guessingGame.settings', JSON.stringify(settings));
154 const toggle = React.useCallback(() => {
157 saveSettings({ autoTrack: false });
160 saveSettings({ autoTrack: true });
165 React.useEffect(() => {
166 if (enabled && !status.error && status.connected && status.device) {
167 const checkInGame = () => {
168 sock.current.readWRAM(GAME_MODE, 1, (data) => {
169 setInGame(IN_GAME_MODES.includes(data[0]));
173 const timer = setInterval(checkInGame, 5000);
175 clearInterval(timer);
180 }, [enabled && !status.error && status.connected && status.device]);
182 // refresh static game information
183 React.useEffect(() => {
185 sock.current.readBytes(SEED_TYPE, 1, (data) => {
186 setSeedType(data[0]);
188 sock.current.readBytes(GT_CRYSTALS, 1, (data) => {
189 setGTCrystals(data[0]);
191 sock.current.readBytes(GANON_TYPE, 1, (data) => {
192 setGanonType(data[0]);
194 sock.current.readBytes(FREE_ITEM_MENU, 1, (data) => {
195 setFreeItemMenu(data[0]);
197 sock.current.readBytes(INIT_SRAM + PYRAMID_SCREEN, 1, (data) => {
198 setPyramidOpen(!!(data[0] & 0x20));
202 const applicable = React.useMemo(() => {
205 GT_TYPES.includes(ganonType) &&
207 !(freeItemMenu & 0x02);
208 }, [freeItemMenu, ganonType, gtCrystals, pyramidOpen, seedType]);
210 // update crystals information
211 React.useEffect(() => {
212 if (!applicable || !inGame || hasBigKey) return;
213 const updateCrystals = () => {
214 sock.current.readWRAM(SAVE_WRAM + OWNED_CRYSTALS, 1, (data) => {
216 for (let i = 0; i < 7; ++i) {
217 if (data[0] & Math.pow(2, i)) {
221 setOwnedCrystals(owned);
224 // increase frequency for the last
225 const timer = setInterval(updateCrystals, ownedCrystals === gtCrystals - 1 ? 1000 : 15000);
227 clearInterval(timer);
229 }, [applicable, gtCrystals, hasBigKey, inGame, ownedCrystals, sock]);
231 // start game once all required crystals have been acquired
232 React.useEffect(() => {
233 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals || hasEntered) return;
234 controls.current.onStart();
235 const updateDungeon = () => {
236 sock.current.readWRAM(CURRENT_DUNGEON, 2, (data) => {
237 setLastEntrance(data[0] + (data[1] * 256));
240 const timer = setInterval(updateDungeon, 1000);
242 clearInterval(timer);
244 }, [applicable, controls, gtCrystals, hasBigKey, hasEntered, ownedCrystals]);
246 // stop game when GT has been entered
247 React.useEffect(() => {
248 if (!applicable || hasBigKey || ownedCrystals !== gtCrystals) return;
249 if (lastEntrance === GT_ENTRANCE_ID) {
250 controls.current.onStop();
253 }, [applicable, controls, gtCrystals, hasBigKey, lastEntrance, ownedCrystals]);
256 React.useEffect(() => {
257 if (!applicable || !hasEntered || hasBigKey) return;
258 const updateGTState = () => {
259 const roomDataSize = ROOM_DATA_END - ROOM_DATA_START;
260 sock.current.readWRAM(SAVE_WRAM + ROOM_DATA_START, roomDataSize, (data) => {
261 const gtState = getGTBasementState(data);
262 const gtCount = countGTBasementState(gtState);
264 const cmp = compareGTBasementState(old.state, gtState);
270 torch: cmp === 'torchSeen' ? gtCount : old.torch,
277 const timer = setInterval(updateGTState, 500);
279 clearInterval(timer);
281 }, [applicable, hasBigKey, hasEntered]);
283 React.useEffect(() => {
284 if (!applicable) return;
286 const solution = basement.last === 'torch' ? basement.torch : basement.count;
287 controls.current.onSolve(solution);
289 sock.current.readWRAM(SAVE_WRAM + BIG_KEYS_1, 1, (data) => {
290 setHasBigKey(!!(data[0] & 0x04));
293 }, [applicable, basement, controls, hasBigKey]);
295 const statusMsg = React.useMemo(() => {
302 if (!status.connected) {
303 return 'disconnected';
305 if (!status.device) {
309 return 'not-in-game';
312 return 'not-applicable';
315 }, [applicable, enabled, inGame, status]);
318 {['disconnected', 'error', 'no-device'].includes(statusMsg) ?
320 className="me-2 text-warning"
322 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
325 {['not-applicable', 'not-in-game'].includes(statusMsg) ?
327 className="me-2 text-info"
329 title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
334 title={t('autoTracking.heading')}
340 GuessingGameAutoTracking.propTypes = {
341 onSolve: PropTypes.func,
342 onStart: PropTypes.func,
343 onStop: PropTypes.func,
346 export default GuessingGameAutoTracking;